Skip to content
Snippets Groups Projects
CallFragment.kt 70.4 KiB
Newer Older
/*
 *  Copyright (C) 2004-2021 Savoir-faire Linux Inc.
 *
 *  Author: Hadrien De Sousa <hadrien.desousa@savoirfairelinux.com>
 *          Adrien Béraud <adrien.beraud@savoirfairelinux.com>
 *
 *  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, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package cx.ring.fragments

import android.Manifest
Maxime Callet's avatar
Maxime Callet committed
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.PendingIntent
import android.app.PictureInPictureParams
import android.app.RemoteAction
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.*
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.util.Log
import android.util.Rational
import android.view.*
import android.view.TextureView.SurfaceTextureListener
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.constraintlayout.widget.ConstraintLayout
Maxime Callet's avatar
Maxime Callet committed
import androidx.core.view.*
import androidx.databinding.DataBindingUtil
import androidx.percentlayout.widget.PercentFrameLayout
Maxime Callet's avatar
Maxime Callet committed
import com.google.android.material.bottomsheet.BottomSheetBehavior
import cx.ring.R
import cx.ring.adapters.ConfParticipantAdapter
import cx.ring.adapters.ConfParticipantAdapter.ConfParticipantSelected
import cx.ring.client.*
import cx.ring.databinding.FragCallBinding
import cx.ring.databinding.ItemParticipantHandContainerBinding
import cx.ring.databinding.ItemParticipantLabelBinding
import cx.ring.mvp.BaseSupportFragment
import cx.ring.plugins.RecyclerPicker.RecyclerPicker
import cx.ring.plugins.RecyclerPicker.RecyclerPickerLayoutManager.ItemSelectedListener
import cx.ring.service.DRingService
import cx.ring.utils.ActionHelper
Adrien Béraud's avatar
Adrien Béraud committed
import cx.ring.utils.ContentUriHandler
import cx.ring.utils.ConversationPath
import cx.ring.utils.DeviceUtils.isTablet
import cx.ring.utils.DeviceUtils.isTv
import cx.ring.utils.MediaButtonsHelper.MediaButtonsHelperCallback
import cx.ring.views.AvatarDrawable
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.rxjava3.disposables.CompositeDisposable
import net.jami.call.CallPresenter
import net.jami.call.CallView
import net.jami.daemon.JamiService
import net.jami.model.Call.CallStatus
import net.jami.model.Conference.ParticipantInfo
import net.jami.model.Contact
import net.jami.model.ContactViewModel
import net.jami.model.Uri
import net.jami.services.DeviceRuntimeService
import net.jami.services.HardwareService
import net.jami.services.HardwareService.AudioState
import net.jami.services.NotificationService
import java.util.*
import javax.inject.Inject
Adrien Béraud's avatar
Adrien Béraud committed
import kotlin.math.max
import kotlin.math.min

@AndroidEntryPoint
class CallFragment : BaseSupportFragment<CallPresenter, CallView>(), CallView,
    MediaButtonsHelperCallback, ItemSelectedListener {
    private var binding: FragCallBinding? = null
    private var mOrientationListener: OrientationEventListener? = null
    private var mScreenWakeLock: PowerManager.WakeLock? = null
    private var mCurrentOrientation = 0
    private var mVideoWidth = -1
    private var mVideoHeight = -1
    private var mPreviewWidth = 720
    private var mPreviewHeight = 1280
    private var mPreviewSurfaceWidth = 0
    private var mPreviewSurfaceHeight = 0
Maxime Callet's avatar
Maxime Callet committed
    private var isInPIP = false
    private lateinit var mProjectionManager: MediaProjectionManager
    private var mBackstackLost = false
    private var confAdapter: ConfParticipantAdapter? = null
    private var mConferenceMode = false
    var isChoosePluginMode = false
        private set
    private var pluginsModeFirst = true
    private var callMediaHandlers: List<String>? = null
    private var previousPluginPosition = -1
    private var rp: RecyclerPicker? = null
    private val animation = ValueAnimator().apply { duration = 150 }
    private var previewDrag: PointF? = null
    private val previewSnapAnimation = ValueAnimator().apply {
        duration = 250
        setFloatValues(0f, 1f)
        interpolator = DecelerateInterpolator()
        addUpdateListener { a -> configurePreview(mPreviewSurfaceWidth, a.animatedFraction) }
    }
    private var previewMargin: Float = 0f
    private val previewMargins = IntArray(4)
    private var previewHiddenState = 0f

    private enum class PreviewPosition { LEFT, RIGHT }
Maxime Callet's avatar
Maxime Callet committed

    private var previewPosition = PreviewPosition.LEFT

    @Inject
    lateinit var mDeviceRuntimeService: DeviceRuntimeService
    private val mCompositeDisposable = CompositeDisposable()
Maxime Callet's avatar
Maxime Callet committed
    private var bottomSheetParams: BottomSheetBehavior<View>? = null
    private var isMyMicMuted: Boolean = false
    override fun initPresenter(presenter: CallPresenter) {
        val args = requireArguments()
        presenter.wantVideo = args.getBoolean(KEY_HAS_VIDEO, false)
        args.getString(KEY_ACTION)?.let { action ->
            if (action == Intent.ACTION_CALL) {
                prepareCall(false)
Maxime Callet's avatar
Maxime Callet committed
            } else if (action == Intent.ACTION_VIEW || action == CallActivity.ACTION_CALL_ACCEPT) {
                presenter.initIncomingCall(
                    args.getString(NotificationService.KEY_CALL_ID)!!,
                    action == Intent.ACTION_VIEW
                )
Maxime Callet's avatar
Maxime Callet committed
        }
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        previewMargin = inflater.context.resources.getDimension(R.dimen.call_preview_margin)
Maxime Callet's avatar
Maxime Callet committed
        return (DataBindingUtil.inflate(inflater, R.layout.frag_call, container, false) as FragCallBinding)
            .also { b ->
                b.presenter = this
                binding = b
                rp = RecyclerPicker(b.recyclerPicker, R.layout.item_picker, LinearLayout.HORIZONTAL, this)
                    .apply { setFirstLastElementsWidths(112, 112) }
                bottomSheetParams = binding?.callOptionsBottomSheet?.let { BottomSheetBehavior.from(it) }
            }.root
Maxime Callet's avatar
Maxime Callet committed
    }

    @SuppressLint("ClickableViewAccessibility", "RtlHardcoded", "WakelockTimeout")
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        setHasOptionsMenu(false)
        super.onViewCreated(view, savedInstanceState)

        val windowManager = view.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
        mCurrentOrientation = windowManager.defaultDisplay.rotation
        val dpRatio = requireActivity().resources.displayMetrics.density
        animation.addUpdateListener { valueAnimator ->
            binding?.let { binding ->
                val upBy = valueAnimator.animatedValue as Int
                val layoutParams = binding.previewContainer.layoutParams as RelativeLayout.LayoutParams
                layoutParams.setMargins(0, 0, 0, (upBy * dpRatio).toInt())
                binding.previewContainer.layoutParams = layoutParams
Maxime Callet's avatar
Maxime Callet committed

        mProjectionManager =
            requireContext().getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        val powerManager = requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager
        mScreenWakeLock = powerManager.newWakeLock(
            PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.ON_AFTER_RELEASE,
            "ring:callLock"
        ).apply {
            setReferenceCounted(false)
            if (!isHeld)
                acquire()
        }

        ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
            setBottomSheet(insets)
            insets
        }

        binding?.let { binding ->
            binding.videoSurface.holder.setFormat(PixelFormat.RGBA_8888)
            binding.videoSurface.holder.addCallback(object : SurfaceHolder.Callback {
                override fun surfaceCreated(holder: SurfaceHolder) {
                    presenter.videoSurfaceCreated(holder)
                }

                override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}

                override fun surfaceDestroyed(holder: SurfaceHolder) {
                    presenter.videoSurfaceDestroyed()
                }
            })
            binding.pluginPreviewSurface.holder.setFormat(PixelFormat.RGBA_8888)
            binding.pluginPreviewSurface.holder.addCallback(object : SurfaceHolder.Callback {
                override fun surfaceCreated(holder: SurfaceHolder) {
                    presenter.pluginSurfaceCreated(holder)
                }

                override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}

                override fun surfaceDestroyed(holder: SurfaceHolder) {
                    presenter.pluginSurfaceDestroyed()
                }
            })

            val insets = ViewCompat.getRootWindowInsets(view)
            insets?.apply {
                presenter.uiVisibilityChanged(this.isVisible(WindowInsetsCompat.Type.navigationBars()))
            }
            view.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> resetVideoSize(mVideoWidth, mVideoHeight) }

            // todo: doublon with CallActivity.onConfigurationChanged ??
            mOrientationListener = object : OrientationEventListener(context) {
                override fun onOrientationChanged(orientation: Int) {
                    val rot = windowManager.defaultDisplay.rotation
                    if (mCurrentOrientation != rot) {
                        mCurrentOrientation = rot
                        presenter.configurationChanged(rot)
                        setRvSize()
Maxime Callet's avatar
Maxime Callet committed
                    }
                }
            }.apply { if (canDetectOrientation()) enable() }

            binding.callSpeakerBtn.isChecked = presenter.isSpeakerphoneOn
            binding.callMicBtn.isChecked = presenter.isMicrophoneMuted
            binding.pluginPreviewSurface.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
                configureTransform(mPreviewSurfaceWidth, mPreviewSurfaceHeight)
            }
            binding.previewSurface.surfaceTextureListener = listener
            binding.previewSurface.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
                configureTransform(mPreviewSurfaceWidth, mPreviewSurfaceHeight)
            }
            binding.previewContainer.setOnTouchListener(previewTouchListener)
            binding.pluginPreviewContainer.setOnTouchListener { v: View, event: MotionEvent ->
                val action = event.actionMasked
                val parent = v.parent as RelativeLayout
                val params = v.layoutParams as RelativeLayout.LayoutParams

                return@setOnTouchListener when (action) {
Maxime Callet's avatar
Maxime Callet committed
                    MotionEvent.ACTION_DOWN -> {
                        previewSnapAnimation.cancel()
                        previewDrag = PointF(event.x, event.y)
                        v.elevation = v.context.resources.getDimension(R.dimen.call_preview_elevation_dragged)
                        params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                        params.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                        params.addRule(RelativeLayout.ALIGN_PARENT_TOP)
                        params.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
                        params.setMargins(
                            v.x.toInt(), v.y.toInt(),
                            parent.width - (v.x.toInt() + v.width),
                            parent.height - (v.y.toInt() + v.height)
                        )
                        v.layoutParams = params
Maxime Callet's avatar
Maxime Callet committed
                    }
                    MotionEvent.ACTION_MOVE -> {
                        if (previewDrag != null) {
                            val currentXPosition = params.leftMargin + (event.x - previewDrag!!.x).toInt()
                            val currentYPosition = params.topMargin + (event.y - previewDrag!!.y).toInt()
                            params.setMargins(
                                currentXPosition, currentYPosition,
                                -(currentXPosition + v.width - event.x.toInt()),
                                -(currentYPosition + v.height - event.y.toInt())
                            )
                            v.layoutParams = params
                            val outPosition = binding.pluginPreviewContainer.width * 0.85f
                            var drapOut = 0f
                            if (currentXPosition < 0) {
                                drapOut = min(1f, -currentXPosition / outPosition)
                            } else if (currentXPosition + v.width > parent.width) {
                                drapOut = min(1f, (currentXPosition + v.width - parent.width) / outPosition)
                            }
                            setPreviewDragHiddenState(drapOut)
                            true
                        } else false
Maxime Callet's avatar
Maxime Callet committed
                    }
                    MotionEvent.ACTION_UP -> {
                        if (previewDrag != null) {
                            val currentXPosition = params.leftMargin + (event.x - previewDrag!!.x).toInt()
                            previewSnapAnimation.cancel()
                            previewDrag = null
                            v.elevation = v.context.resources.getDimension(R.dimen.call_preview_elevation)
                            var ml = 0;
                            var mr = 0;
                            var mt = 0;
                            var mb = 0
                            val hp = binding.pluginPreviewHandle.layoutParams as FrameLayout.LayoutParams
                            if (params.leftMargin + v.width / 2 > parent.width / 2) {
                                params.removeRule(RelativeLayout.ALIGN_PARENT_LEFT)
                                params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                                mr = (parent.width - v.width - v.x).toInt()
                                previewPosition = PreviewPosition.RIGHT
                                hp.gravity = Gravity.CENTER_VERTICAL or Gravity.LEFT
                            } else {
                                params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                                params.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
                                ml = v.x.toInt()
                                previewPosition = PreviewPosition.LEFT
                                hp.gravity = Gravity.CENTER_VERTICAL or Gravity.RIGHT
                            }
                            binding.pluginPreviewHandle.layoutParams = hp
                            if (params.topMargin + v.height / 2 > parent.height / 2) {
                                params.removeRule(RelativeLayout.ALIGN_PARENT_TOP)
                                params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                                mb = (parent.height - v.height - v.y).toInt()
                            } else {
                                params.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                                params.addRule(RelativeLayout.ALIGN_PARENT_TOP)
                                mt = v.y.toInt()
                            }
                            previewMargins[0] = ml
                            previewMargins[1] = mt
                            previewMargins[2] = mr
                            previewMargins[3] = mb
                            params.setMargins(ml, mt, mr, mb)
                            v.layoutParams = params
                            val outPosition = binding.pluginPreviewContainer.width * 0.85f
                            previewHiddenState = when {
                                currentXPosition < 0 -> min(1f, -currentXPosition / outPosition)
                                currentXPosition + v.width > parent.width -> min(
                                    1f,
                                    (currentXPosition + v.width - parent.width) / outPosition
                                )
                                else -> 0f
                            }
                            setPreviewDragHiddenState(previewHiddenState)
                            previewSnapAnimation.start()
                            true
                        } else false
                    else -> false
            binding.dialpadEditText.addTextChangedListener(object : TextWatcher {
Maxime Callet's avatar
Maxime Callet committed
                  override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
                  override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
                      if (before == 0)
                        presenter.sendDtmf(s.subSequence(start, start + count))
                  override fun afterTextChanged(s: Editable) {
                      if (s.isNotEmpty())
                        s.clear()
                  }
              })
    fun setRvSize() {
        val binding = binding ?: return
        val dm = resources.displayMetrics
        val orientation = resources.configuration.orientation
        val gridViewHeight = binding.callParametersGrid.height

        // define recyclerview maxheight based on screen orientation
        val mConstrainLayout = binding.confControlGroup
        val lp = mConstrainLayout.layoutParams as ConstraintLayout.LayoutParams
        lp.matchConstraintMaxHeight =
            if (orientation == Configuration.ORIENTATION_LANDSCAPE)
                dm.heightPixels - gridViewHeight - (100 * dm.density).toInt()
            else (dm.heightPixels - gridViewHeight * 0.8).toInt()
        mConstrainLayout.layoutParams = lp
    }

    override fun onUserLeave() {
        presenter.requestPipMode()
    }

Maxime Callet's avatar
Maxime Callet committed
    override fun onStop() {
        super.onStop()
        previewSnapAnimation.cancel()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        mOrientationListener?.disable()
        mOrientationListener = null
Maxime Callet's avatar
Maxime Callet committed
        mCompositeDisposable.clear()
        mScreenWakeLock?.let {
            if (it.isHeld)
                it.release()
            mScreenWakeLock = null
Maxime Callet's avatar
Maxime Callet committed
        }
        binding = null
    }

    override fun onDestroy() {
        super.onDestroy()
        mCompositeDisposable.dispose()
    }


    //todo: enable pip when only our video is displayed
    override fun enterPipMode(callId: String) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
            return
        val context = requireContext()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val binding = binding ?: return
            if (binding.videoSurface.visibility != View.VISIBLE)
            val l = IntArray(2).apply { binding.videoSurface.getLocationInWindow(this) }
            val x = l[0]
            val y = l[1]
            val w = binding.videoSurface.width
            val h = binding.videoSurface.height
                requireActivity().enterPictureInPictureMode(PictureInPictureParams.Builder()
                    .setAspectRatio(Rational(w, h))
                    .setSourceRectHint(Rect(x, y, x + w, y + h))
                    .setActions(listOf(RemoteAction(
                        Icon.createWithResource(context, R.drawable.baseline_call_end_24),
                        getString(R.string.action_call_hangup),
                        getString(R.string.action_call_hangup),
                        PendingIntent.getService(
                            context,
                            Random().nextInt(),
                            Intent(DRingService.ACTION_CALL_END)
                                .setClass(context, JamiService::class.java)
                                .putExtra(NotificationService.KEY_CALL_ID, callId), ContentUriHandler.immutable(PendingIntent.FLAG_ONE_SHOT)
                        )
                    )))
                    .build())
            } catch (e: Exception) {
                Log.w(TAG, "Can't enter  PIP mode", e)
            }
        } else if (isTv(context)) {
            requireActivity().enterPictureInPictureMode()
        }
    }

Maxime Callet's avatar
Maxime Callet committed
    override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
        isInPIP = isInPictureInPictureMode
        if (isInPictureInPictureMode) {
            binding!!.callCoordinatorOptionContainer.visibility = View.GONE
            val callActivity = activity as CallActivity?
            callActivity?.hideSystemUI()
            binding!!.pluginPreviewContainer.visibility = View.GONE
            binding!!.pluginPreviewSurface.visibility = View.GONE
            binding!!.previewContainer.visibility = View.GONE
            binding!!.previewSurface.visibility = View.GONE
Maxime Callet's avatar
Maxime Callet committed
        } else {
            mBackstackLost = true
            binding!!.callCoordinatorOptionContainer.visibility = View.VISIBLE
            binding!!.pluginPreviewContainer.visibility = View.VISIBLE
            binding!!.pluginPreviewSurface.visibility = View.VISIBLE
            binding!!.previewContainer.visibility = View.VISIBLE
            binding!!.previewSurface.visibility = View.VISIBLE
        }
    }

    private val listener: SurfaceTextureListener = object : SurfaceTextureListener {
        override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
Maxime Callet's avatar
Maxime Callet committed
            Log.w(TAG, " onSurfaceTextureAvailable -------->  width: $width, height: $height")
            mPreviewSurfaceWidth = width
            mPreviewSurfaceHeight = height
Maxime Callet's avatar
Maxime Callet committed
            Log.w(
                TAG,
                " onSurfaceTextureAvailable -------->  mPreviewSurfaceWidth: $mPreviewSurfaceWidth, mPreviewSurfaceHeight: $mPreviewSurfaceHeight"
            )
            presenter.previewVideoSurfaceCreated(binding!!.previewSurface)
        }

        override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
Maxime Callet's avatar
Maxime Callet committed
            Log.w(TAG, " onSurfaceTextureSizeChanged ------>  width: $width, height: $height")
            mPreviewSurfaceWidth = width
            mPreviewSurfaceHeight = height
            configurePreview(width, 1f)
        }

        override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
            presenter.previewVideoSurfaceDestroyed()
            return true
        }

        override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
    }

    /**
     * @param hiddenState 0.f if fully shown, 1.f if fully hidden.
     */
    private fun setPreviewDragHiddenState(hiddenState: Float) {
        binding?.let { binding ->
            binding.previewSurface.alpha = 1f - 3 * hiddenState / 4
            binding.pluginPreviewSurface.alpha = 1f - 3 * hiddenState / 4
            binding.previewHandle.alpha = hiddenState
            binding.pluginPreviewHandle.alpha = hiddenState
        }
    }

Maxime Callet's avatar
Maxime Callet committed
    private val previewTouchListener = object : View.OnTouchListener {
        @SuppressLint("ClickableViewAccessibility")
        override fun onTouch(v: View, event: MotionEvent): Boolean {
            val action = event.actionMasked
            val parent = v.parent as RelativeLayout
            val params = v.layoutParams as RelativeLayout.LayoutParams
            when (action) {
                MotionEvent.ACTION_DOWN -> {
                    previewSnapAnimation.cancel()
                    previewDrag = PointF(event.x, event.y)
                    v.elevation = v.context.resources.getDimension(R.dimen.call_preview_elevation_dragged)
                    params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                    params.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                    params.addRule(RelativeLayout.ALIGN_PARENT_TOP)
                    params.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
                    params.setMargins(
                        v.x.toInt(),
                        v.y.toInt(),
                        parent.width - (v.x.toInt() + v.width),
                        parent.height - (v.y.toInt() + v.height)
                    )
                    v.layoutParams = params
                    return true
                }
                MotionEvent.ACTION_MOVE -> {
                    if (previewDrag != null) {
                        val currentXPosition = params.leftMargin + (event.x - previewDrag!!.x).toInt()
                        val currentYPosition = params.topMargin + (event.y - previewDrag!!.y).toInt()
                        params.setMargins(
                            currentXPosition,
                            currentYPosition,
                            -(currentXPosition + v.width - event.x.toInt()),
                            -(currentYPosition + v.height - event.y.toInt())
                        )
                        v.layoutParams = params
                        val outPosition = binding!!.previewContainer.width * 0.85f
                        var drapOut = 0f
                        if (currentXPosition < 0) {
                            drapOut = min(1f, -currentXPosition / outPosition)
                        } else if (currentXPosition + v.width > parent.width) {
                            drapOut = min(1f, (currentXPosition + v.width - parent.width) / outPosition)
                        }
                        setPreviewDragHiddenState(drapOut)
                        return true
                    }
                    return false
                }
                MotionEvent.ACTION_UP -> {
                    if (previewDrag != null) {
                        val currentXPosition = params.leftMargin + (event.x - previewDrag!!.x).toInt()
                        previewSnapAnimation.cancel()
                        previewDrag = null
                        v.elevation = v.context.resources.getDimension(R.dimen.call_preview_elevation)
                        var ml = 0
                        var mr = 0
                        var mt = 0
                        var mb = 0
                        val hp = binding!!.previewHandle.layoutParams as FrameLayout.LayoutParams
                        if (params.leftMargin + v.width / 2 > parent.width / 2) {
                            params.removeRule(RelativeLayout.ALIGN_PARENT_LEFT)
                            params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                            mr = (parent.width - v.width - v.x).toInt()
                            previewPosition = PreviewPosition.RIGHT
                            hp.gravity = Gravity.CENTER_VERTICAL or Gravity.LEFT
                        } else {
                            params.removeRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                            params.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
                            ml = v.x.toInt()
                            previewPosition = PreviewPosition.LEFT
                            hp.gravity = Gravity.CENTER_VERTICAL or Gravity.RIGHT
                        }
                        binding!!.previewHandle.layoutParams = hp
                        if (params.topMargin + v.height / 2 > parent.height / 2) {
                            params.removeRule(RelativeLayout.ALIGN_PARENT_TOP)
                            params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                            mb = (parent.height - v.height - v.y).toInt()
                        } else {
                            params.removeRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                            params.addRule(RelativeLayout.ALIGN_PARENT_TOP)
                            mt = v.y.toInt()
                        }
                        previewMargins[0] = ml
                        previewMargins[1] = mt
                        previewMargins[2] = mr
                        previewMargins[3] = mb
                        params.setMargins(ml, mt, mr, mb)
                        v.layoutParams = params
                        val outPosition = binding!!.previewContainer.width * 0.85f
                        previewHiddenState = when {
                            currentXPosition < 0 ->
                                min(1f, -currentXPosition / outPosition)
                            currentXPosition + v.width > parent.width ->
                                min(1f, (currentXPosition + v.width - parent.width) / outPosition)
                            else -> 0f
                        }
                        setPreviewDragHiddenState(previewHiddenState)
                        previewSnapAnimation.start()
                        return true
                    }
                    return false
                }
                else -> return false
            }
        }
    }

    private fun configurePreview(width: Int, animatedFraction: Float) {
Maxime Callet's avatar
Maxime Callet committed
        Log.w(TAG, " configurePreview --------->  width: $width, animatedFraction: $animatedFraction")
        val binding = binding ?: return
        val params = binding.previewContainer.layoutParams as RelativeLayout.LayoutParams
        val r = 1f - animatedFraction
        var hideMargin = 0f
        var targetHiddenState = 0f
        if (previewHiddenState > 0f) {
            targetHiddenState = 1f
            val v = width * 0.85f * animatedFraction
            hideMargin = if (previewPosition == PreviewPosition.RIGHT) v else -v
        }
        setPreviewDragHiddenState(previewHiddenState * r + targetHiddenState * animatedFraction)
        val f = previewMargin * animatedFraction
        params.setMargins(
            (previewMargins[0] * r + f + hideMargin).toInt(),
            (previewMargins[1] * r + f).toInt(),
            (previewMargins[2] * r + f - hideMargin).toInt(),
            (previewMargins[3] * r + f).toInt()
        )
        binding.previewContainer.layoutParams = params
        binding.pluginPreviewContainer.layoutParams = params
    }

    /**
     * Releases current wakelock and acquires a new proximity wakelock if current call is audio only.
     *
     * @param isAudioOnly true if it is an audio call
     */
    @SuppressLint("WakelockTimeout")
    override fun handleCallWakelock(isAudioOnly: Boolean) {
        if (isAudioOnly) {
            mScreenWakeLock?.apply {
                if (isHeld) release()
            }
            val powerManager = requireContext().getSystemService(Context.POWER_SERVICE) as PowerManager
            mScreenWakeLock = powerManager.newWakeLock(
                PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.ON_AFTER_RELEASE,
            ).apply {
                setReferenceCounted(false)
                if (!isHeld)
                    acquire()
            }
        }
    }


Maxime Callet's avatar
Maxime Callet committed
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode != REQUEST_PERMISSION_INCOMING && requestCode != REQUEST_PERMISSION_OUTGOING) return
        var i = 0
        val n = permissions.size

        val hasVideo = presenter.wantVideo

        while (i < n) {
            val audioGranted = mDeviceRuntimeService.hasAudioPermission()
            val granted = grantResults[i] == PackageManager.PERMISSION_GRANTED
            when (permissions[i]) {
                Manifest.permission.CAMERA -> {
                    presenter.cameraPermissionChanged(granted)
                    if (audioGranted) {
                        initializeCall(requestCode == REQUEST_PERMISSION_INCOMING, hasVideo)
                    }
                }
                Manifest.permission.RECORD_AUDIO -> {
                    presenter.audioPermissionChanged(granted)
                    initializeCall(requestCode == REQUEST_PERMISSION_INCOMING, hasVideo)
                }
            }
            i++
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Maxime Callet's avatar
Maxime Callet committed
        Log.w(TAG, "[screenshare] onActivityResult ---> requestCode: $requestCode, resultCode: $resultCode")
        when (requestCode) {
Maxime Callet's avatar
Maxime Callet committed
            REQUEST_CODE_ADD_PARTICIPANT -> {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val path = ConversationPath.fromUri(data.data)
                    if (path != null) {
                        presenter.addConferenceParticipant(path.accountId, path.conversationUri)
                    }
Maxime Callet's avatar
Maxime Callet committed
            REQUEST_CODE_SCREEN_SHARE -> {
                Log.w(TAG, "[screenshare] onActivityResult ---> requestCode: $requestCode, resultCode: $resultCode")
                if (resultCode == Activity.RESULT_OK && data != null) {
                    try {
                        startScreenShare(mProjectionManager.getMediaProjection(resultCode, data))
                    } catch (e: Exception) {
                        Log.w(TAG, "Error starting screen sharing", e)
                    }
                } else {
                    binding!!.callSharescreenBtn.isChecked = false
                }
            }
        }
    }

    override fun displayContactBubble(display: Boolean) {
            contactBubbleLayout.isVisible = display
    override fun displayPeerVideo(display: Boolean) {
Maxime Callet's avatar
Maxime Callet committed
        Log.w(TAG, "displayPeerVideo -> $display")
        binding!!.videoSurface.isVisible = display
        displayContactBubble(!display)
    }
    override fun displayLocalVideo(display: Boolean) {
Maxime Callet's avatar
Maxime Callet committed
        Log.w(TAG, "displayLocalVideo -> $display")
        binding?.apply {
            val pluginMode = isChoosePluginMode
            previewContainer.isVisible = !pluginMode && display
            pluginPreviewContainer.isVisible = pluginMode && display
            pluginPreviewSurface.isVisible = pluginMode && display
            if (pluginMode) pluginPreviewSurface.setZOrderMediaOverlay(true)
Aline Gondim Santos's avatar
Aline Gondim Santos committed
        }
    }

    override fun displayHangupButton(display: Boolean) {
        Log.w(TAG, "displayHangupButton $display")
        /* binding?.apply { confControlGroup.visibility = when {
            !mConferenceMode -> View.GONE
            display && !isChoosePluginMode -> View.VISIBLE
            else -> View.INVISIBLE
        }} */
    }

    override fun displayDialPadKeyboard() {
        val binding = binding ?: return
        binding.dialpadEditText.requestFocus()
        val imm = binding.dialpadEditText.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        imm.showSoftInput(binding.dialpadEditText, InputMethodManager.SHOW_FORCED)
        //imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
    fun switchCamera() {
Maxime Callet's avatar
Maxime Callet committed
        binding!!.callSpeakerBtn.isChecked = false
        presenter.switchOnOffCamera()
    }

    override fun updateAudioState(state: AudioState) {
        binding!!.callSpeakerBtn.isChecked = state.outputType == HardwareService.AudioOutput.SPEAKERS
    }

    override fun updateTime(duration: Long) {
        binding?.let { binding ->
Maxime Callet's avatar
Maxime Callet committed
            binding.callStatusTxt.text = if (duration <= 0) null else String.format(
                "%d:%02d:%02d",
                duration / 3600,
                duration % 3600 / 60,
Maxime Callet's avatar
Maxime Callet committed
                duration % 60
            )
        }
    }

    @SuppressLint("RestrictedApi")
    override fun updateConfInfo(participantInfo: List<ParticipantInfo>) {
        val binding = binding ?: return
Maxime Callet's avatar
Maxime Callet committed
        mConferenceMode = participantInfo.isNotEmpty()

        if (participantInfo.isNotEmpty()) {
Maxime Callet's avatar
Maxime Callet committed
            isMyMicMuted = participantInfo[0].audioLocalMuted
            val username = if (participantInfo.size > 1)
                "Conference with ${participantInfo.size} people"
            else participantInfo[0].contact.displayName
            val displayName = if (participantInfo.size > 1) null else participantInfo[0].contact.displayName
            val hasProfileName = displayName != null && !displayName.contentEquals(username)
Maxime Callet's avatar
Maxime Callet committed
            val activity = activity
            if (activity != null) {
                val call = participantInfo[0].call
                if (call != null) {
                    val conversationUri = if (call.conversationId != null)
                        Uri(Uri.SWARM_SCHEME, call.conversationId!!)
                    else call.contact!!.conversationUri.blockingFirst()
Maxime Callet's avatar
Maxime Callet committed
                    activity.intent = Intent(
                        Intent.ACTION_VIEW,
                        ConversationPath.toUri(call.account!!, conversationUri), context, CallActivity::class.java
                    )
                        .apply { putExtra(NotificationService.KEY_CALL_ID, call.confId ?: call.daemonIdString) }
                    arguments = Bundle().apply {
                        putString(KEY_ACTION, Intent.ACTION_VIEW)
                        putString(NotificationService.KEY_CALL_ID, call.confId ?: call.daemonIdString)
                    }
Maxime Callet's avatar
Maxime Callet committed
                } else {
                    Log.w(TAG, "DEBUG null call")
                }
            } else {
                Log.w(TAG, "DEBUG null activity")
            }
            if (hasProfileName) {
                binding.contactBubbleNumTxt.visibility = View.VISIBLE
                binding.contactBubbleTxt.text = displayName
                binding.contactBubbleNumTxt.text = username
            } else {
                binding.contactBubbleNumTxt.visibility = View.GONE
                binding.contactBubbleTxt.text = username
            }
Maxime Callet's avatar
Maxime Callet committed
            binding.contactBubble.setImageDrawable(
                AvatarDrawable.Builder()
                    .withContact(participantInfo[0].contact)
                    .withCircleCrop(true)
                    .withPresence(false)
                    .build(requireActivity())
            )
            generateParticipantOverlay(participantInfo)
Maxime Callet's avatar
Maxime Callet committed

        binding.confControlGroup.visibility = View.VISIBLE
        confAdapter?.apply {
            updateFromCalls(participantInfo)
        }
        // Create new adapter
            ?: ConfParticipantAdapter(participantInfo, object : ConfParticipantSelected {
                override fun onParticipantSelected(
                    contact: ParticipantInfo,
                    action: ConfParticipantAdapter.ParticipantAction
                ) {
                    when (action) {
                        ConfParticipantAdapter.ParticipantAction.ShowDetails -> presenter.openParticipantContact(contact)
                        ConfParticipantAdapter.ParticipantAction.Hangup -> presenter.hangupParticipant(contact)
                        ConfParticipantAdapter.ParticipantAction.Mute -> presenter.muteParticipant(
                            contact,
                            !contact.audioModeratorMuted
                        )
                        ConfParticipantAdapter.ParticipantAction.Extend -> presenter.maximizeParticipant(contact)
                    }
                }
            }).apply {
                setHasStableIds(true)
                confAdapter = this
                binding.confControlGroup.adapter = this
        binding.root.post { setBottomSheet() }
    private fun generateParticipantOverlay(participantsInfo: List<ParticipantInfo>) {
        val overlayViewBinding = binding?.participantOverlayContainer ?: return
        overlayViewBinding.removeAllViews()
        overlayViewBinding.visibility = if (participantsInfo.isEmpty()) View.GONE else View.VISIBLE

        val inflater = LayoutInflater.from(overlayViewBinding.context)

        for (i in participantsInfo) {
            // adding name, mic etc..
            val displayName = i.contact.displayName
            if (!TextUtils.isEmpty(displayName)) {
                val participantInfoOverlay = ItemParticipantLabelBinding.inflate(inflater)
                val infoOverlayLayoutParams = PercentFrameLayout.LayoutParams(
                    ViewGroup.LayoutParams.WRAP_CONTENT,
                    ViewGroup.LayoutParams.MATCH_PARENT
                )
                infoOverlayLayoutParams.percentLayoutInfo.startMarginPercent = i.x / mVideoWidth.toFloat()
                infoOverlayLayoutParams.percentLayoutInfo.endMarginPercent = 1f - (i.x + i.w) / mVideoWidth.toFloat()
                infoOverlayLayoutParams.percentLayoutInfo.topMarginPercent = i.y / mVideoHeight.toFloat()
                infoOverlayLayoutParams.percentLayoutInfo.bottomMarginPercent =
                    1f - (i.y + i.h) / mVideoHeight.toFloat()

                participantInfoOverlay.participantName.text = displayName
                //label.moderator.isVisible = i.isModerator
                participantInfoOverlay.mute.isVisible = i.audioModeratorMuted || i.audioLocalMuted
                overlayViewBinding.addView(participantInfoOverlay.root, infoOverlayLayoutParams)
            }

            val raisedHandBadge = ItemParticipantHandContainerBinding.inflate(inflater)
            val raisedHandBadgeLayoutParam = PercentFrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
            )
            raisedHandBadgeLayoutParam.percentLayoutInfo.startMarginPercent = i.x / mVideoWidth.toFloat()
            raisedHandBadgeLayoutParam.percentLayoutInfo.endMarginPercent = 1f - (i.x + i.w) / mVideoWidth.toFloat()
            raisedHandBadgeLayoutParam.percentLayoutInfo.topMarginPercent = i.y / mVideoHeight.toFloat()

            raisedHandBadge.raisedHand.isVisible = i.isHandRaised
            overlayViewBinding.addView(raisedHandBadge.root, raisedHandBadgeLayoutParam)

        }
    }


    override fun updateParticipantRecording(contacts: List<ContactViewModel>) {
        binding?.let { binding ->
            if (contacts.isEmpty()) {
                binding.recordLayout.visibility = View.INVISIBLE
                binding.recordIndicator.clearAnimation()
                return
            }
            val names = StringBuilder()
            val contact = contacts.iterator()
            for (i in contacts.indices) {
                names.append(" ").append(contact.next().displayName)
                if (i != contacts.size - 1) {
                    names.append(",")
                }
            }
            binding.recordLayout.visibility = View.VISIBLE
            binding.recordIndicator.animation = blinkingAnimation
            binding.recordName.text = getString(R.string.remote_recording, names)
        }
    }

    override fun updateCallStatus(callState: CallStatus) {
        binding!!.callStatusTxt.setText(callStateToHumanState(callState))
    /** Receive data from the presenter in order to display valid button to the user */
Maxime Callet's avatar
Maxime Callet committed
    override fun updateBottomSheetButtonStatus(
        isConference: Boolean,
Maxime Callet's avatar
Maxime Callet committed
        isSpeakerOn: Boolean,
        isMicrophoneMuted: Boolean,
        hasMultipleCamera: Boolean,
        canDial: Boolean,
        showPluginBtn: Boolean,
        onGoingCall: Boolean,
        hasActiveVideo: Boolean
            callPluginsBtn.isClickable = showPluginBtn
            callRaiseHandBtn.isClickable = mConferenceMode
            callDialpadBtn.isClickable = canDial
            dialpadBtnContainer.isVisible = canDial
Maxime Callet's avatar
Maxime Callet committed
            callVideocamBtn.apply {
                isChecked = !hasActiveVideo
                setImageResource(if (isChecked) R.drawable.baseline_videocam_off_24 else R.drawable.baseline_videocam_on_24)
Maxime Callet's avatar
Maxime Callet committed
            }
            callCameraFlipBtn.apply {
                isEnabled = !callVideocamBtn.isChecked
                setImageResource(if (hasMultipleCamera && hasActiveVideo) R.drawable.baseline_flip_camera_24 else R.drawable.baseline_flip_camera_24_off)
            }
            callMicBtn.isChecked = isMicrophoneMuted
            callSpeakerBtn.isChecked = isSpeakerOn
Maxime Callet's avatar
Maxime Callet committed
    /**
     * Set bottom sheet, define height for each state (Expanded/Half-expanded/Collapsed) based on current Display metrics (density & size)
Maxime Callet's avatar
Maxime Callet committed
     */
    private fun setBottomSheet(newInset: WindowInsetsCompat? = null) {
        val binding = binding ?: return
        val bsView = binding.callOptionsBottomSheet
        val bsHeight = binding.constraintBsContainer.height
        if (isInPIP || !bsView.isVisible) return

        val dm = resources.displayMetrics
        val density = dm.density
        val screenHeight = binding.callCoordinatorOptionContainer.height
        val gridViewHeight = binding.callParametersGrid.height
        val land = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE

        // define bottomsheet width based on screen orientation
        val bsViewParam = bsView.layoutParams
        bsViewParam.width = if (land) getBottomSheetMaxWidth(dm.widthPixels, density) else -1
        bsView.layoutParams = bsViewParam

        val inset = newInset ?: ViewCompat.getRootWindowInsets(requireView()) ?: return
        val bottomInsets = inset.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.navigationBars()).bottom
        val topInsets = inset.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.statusBars()).top

        val desiredPeekHeight = if (land) (10f * density) + (gridViewHeight / 2f) else (10f * density) + (gridViewHeight / 2f) + bottomInsets
        val halfRatio = ((10f * density) + gridViewHeight + bottomInsets) / screenHeight

        val fullyExpandedOffset = if (screenHeight <= bsHeight + bottomInsets)
                (50 * density).toInt()
        else
                (screenHeight - bsHeight - bottomInsets)
        binding.callCoordinatorOptionContainer.updatePadding(bottom = if (land) 0 else bottomInsets)