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
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.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
import androidx.databinding.DataBindingUtil
import androidx.percentlayout.widget.PercentFrameLayout
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
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.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
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
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 }
private var previewPosition = PreviewPosition.LEFT
@Inject
lateinit var mDeviceRuntimeService: DeviceRuntimeService
private val mCompositeDisposable = CompositeDisposable()
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 ->
} else if (action == Intent.ACTION_VIEW || action == CallActivity.ACTION_CALL_ACCEPT) {
presenter.initIncomingCall(
args.getString(NotificationService.KEY_CALL_ID)!!,
action == Intent.ACTION_VIEW
)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
previewMargin = inflater.context.resources.getDimension(R.dimen.call_preview_margin)
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
}
@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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
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)
}
}
}.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) {
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
}
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)
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
}
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()
binding.dialpadEditText.addTextChangedListener(object : TextWatcher {
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()
}
override fun onStop() {
super.onStop()
previewSnapAnimation.cancel()
}
override fun onDestroyView() {
super.onDestroyView()
mOrientationListener?.disable()
mOrientationListener = null
mScreenWakeLock?.let {
if (it.isHeld)
it.release()
mScreenWakeLock = null
}
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()
}
}
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
} 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) {
Log.w(TAG, " onSurfaceTextureAvailable --------> width: $width, height: $height")
mPreviewSurfaceWidth = width
mPreviewSurfaceHeight = height
Log.w(
TAG,
" onSurfaceTextureAvailable --------> mPreviewSurfaceWidth: $mPreviewSurfaceWidth, mPreviewSurfaceHeight: $mPreviewSurfaceHeight"
)
presenter.previewVideoSurfaceCreated(binding!!.previewSurface)
}
override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
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
}
}
private val previewTouchListener = object : View.OnTouchListener {
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
@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) {
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()
}
}
}
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?) {
Log.w(TAG, "[screenshare] onActivityResult ---> requestCode: $requestCode, resultCode: $resultCode")
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)
}
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) {
binding!!.videoSurface.isVisible = display
displayContactBubble(!display)
}
override fun displayLocalVideo(display: Boolean) {
binding?.apply {
val pluginMode = isChoosePluginMode
previewContainer.isVisible = !pluginMode && display
pluginPreviewContainer.isVisible = pluginMode && display
pluginPreviewSurface.isVisible = pluginMode && display
if (pluginMode) pluginPreviewSurface.setZOrderMediaOverlay(true)
}
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)
}
override fun updateAudioState(state: AudioState) {
binding!!.callSpeakerBtn.isChecked = state.outputType == HardwareService.AudioOutput.SPEAKERS
}
override fun updateTime(duration: Long) {
binding?.let { binding ->
binding.callStatusTxt.text = if (duration <= 0) null else String.format(
"%d:%02d:%02d",
duration / 3600,
duration % 3600 / 60,
}
}
@SuppressLint("RestrictedApi")
override fun updateConfInfo(participantInfo: List<ParticipantInfo>) {
val binding = binding ?: return
mConferenceMode = participantInfo.isNotEmpty()
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)
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()
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)
}
} 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
}
binding.contactBubble.setImageDrawable(
AvatarDrawable.Builder()
.withContact(participantInfo[0].contact)
.withCircleCrop(true)
.withPresence(false)
.build(requireActivity())
)
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 */
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
callVideocamBtn.apply {
isChecked = !hasActiveVideo
setImageResource(if (isChecked) R.drawable.baseline_videocam_off_24 else R.drawable.baseline_videocam_on_24)
}
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
* Set bottom sheet, define height for each state (Expanded/Half-expanded/Collapsed) based on current Display metrics (density & size)
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 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)