Commit b8f112fa authored by Adrien Béraud's avatar Adrien Béraud Committed by Maxime Callet
Browse files

Call: simplify conference logic

Change-Id: I9eec589d734ed08ee2fe96e1244a14c3cdcee487
parent 85a5a060
......@@ -19,6 +19,7 @@
*/
package cx.ring.adapters
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
......@@ -30,68 +31,74 @@ import cx.ring.views.AvatarDrawable
import cx.ring.views.ParticipantView
import net.jami.model.Call
import net.jami.model.Conference.ParticipantInfo
import java.util.*
class ConfParticipantAdapter(private val onSelectedCallback: ConfParticipantSelected) :
class ConfParticipantAdapter(private var calls: List<ParticipantInfo>, private val onSelectedCallback: ConfParticipantSelected) :
RecyclerView.Adapter<ParticipantView>() {
private var calls: List<ParticipantInfo>? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ParticipantView {
return ParticipantView(ItemConferenceParticipantBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ParticipantView, position: Int) {
val info = calls!![position]
val info = calls[position]
val contact = info.contact
val context = holder.itemView.context
val call = info.call
if (call != null && call.callStatus != Call.CallStatus.CURRENT) {
holder.binding.displayName.text = String.format("%s\n%s", contact.displayName, context.getText(CallFragment.callStateToHumanState(call.callStatus)))
val displayName = TextUtils.ellipsize(contact.displayName,
holder.binding.displayName.paint,
holder.binding.displayName.maxWidth.toFloat(),
TextUtils.TruncateAt.MIDDLE)
if (call != null && info.pending) {
holder.binding.displayName.text = String.format("%s\n%s", displayName, context.getText(CallFragment.callStateToHumanState(call.callStatus)))
holder.binding.photo.alpha = .5f
} else {
holder.binding.displayName.text = contact.displayName
holder.binding.displayName.text = displayName
holder.binding.photo.alpha = 1f
}
holder.disposable?.dispose()
holder.binding.photo.setImageDrawable(AvatarDrawable.Builder()
.withContact(contact)
.withCircleCrop(true)
.withPresence(false)
.build(context))
/*;
holder.disposable = AvatarFactory.getAvatar(context, contact)
.withContact(contact)
.withCircleCrop(true)
.withPresence(false)
.build(context))
/*holder.disposable = AvatarFactory.getAvatar(context, contact)
.subscribe(holder.binding.photo::setImageDrawable);*/
holder.itemView.setOnClickListener { view: View ->
onSelectedCallback.onParticipantSelected(view, info)
}
}
override fun getItemId(position: Int): Long {
val info = calls[position]
return Objects.hash(info.contact.uri, info.call?.daemonIdString).toLong()
}
override fun getItemCount(): Int {
return if (calls == null) 0 else calls!!.size
return calls.size
}
fun updateFromCalls(contacts: List<ParticipantInfo>) {
val oldCalls = calls
calls = contacts
if (oldCalls != null) {
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldCalls.size
}
DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return oldCalls.size
}
override fun getNewListSize(): Int {
return contacts.size
}
override fun getNewListSize(): Int {
return contacts.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldCalls[oldItemPosition] === contacts[newItemPosition]
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldCalls[oldItemPosition].contact === contacts[newItemPosition].contact
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return false
}
}).dispatchUpdatesTo(this)
} else {
notifyDataSetChanged()
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return false
}
}).dispatchUpdatesTo(this)
}
interface ConfParticipantSelected {
......
......@@ -313,62 +313,6 @@ class TVCallFragment : BaseSupportFragment<CallPresenter, CallView>(), CallView
)
}
override fun updateContactBubble(calls: List<Call>) {
mConferenceMode = calls.size > 1
val contact = calls[0].contact!!
val username = if (mConferenceMode) "Conference with " + calls.size + " people" else contact.ringUsername
val displayName = if (mConferenceMode) null else contact.displayName
Log.d(TAG, "updateContactBubble: username=" + username + ", uri=" + contact.uri + " photo:" + contact.photo)
mSession?.setMetadata(MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayName)
.build())
val hasProfileName = displayName != null && !displayName.contentEquals(username)
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(contact)
.withCircleCrop(true)
.build(requireActivity())
)
/*if (!mConferenceMode) {
binding.confControlGroup.setVisibility(View.GONE);
} else {
binding.confControlGroup.setVisibility(View.VISIBLE);
if (confAdapter == null) {
confAdapter = new ConfParticipantAdapter((view, call) -> {
Context context = requireContext();
PopupMenu popup = new PopupMenu(context, view);
popup.inflate(R.menu.conference_participant_actions);
popup.setOnMenuItemClickListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.conv_contact_details) {
presenter.openParticipantContact(call);
} else if (itemId == R.id.conv_contact_hangup) {
presenter.hangupParticipant(call);
} else {
return false;
}
return true;
});
MenuPopupHelper menuHelper = new MenuPopupHelper(context, (MenuBuilder) popup.getMenu(), view);
menuHelper.setForceShowIcon(true);
menuHelper.show();
});
}
confAdapter.updateFromCalls(calls);
if (binding.confControlGroup.getAdapter() == null)
binding.confControlGroup.setAdapter(confAdapter);
}*/
}
override fun updateCallStatus(callStatus: CallStatus) {
when (callStatus) {
CallStatus.NONE -> binding!!.callStatusTxt.text = ""
......@@ -540,20 +484,14 @@ class TVCallFragment : BaseSupportFragment<CallPresenter, CallView>(), CallView
}
override fun goToContact(accountId: String, contact: Contact) {
startActivity(Intent(Intent.ACTION_VIEW,
android.net.Uri.withAppendedPath(
android.net.Uri.withAppendedPath(
ContentUriHandler.CONTACT_CONTENT_URI,
accountId
), contact.primaryNumber))
.setClass(requireContext(), ContactDetailsActivity::class.java)
)
startActivity(Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, contact.uri), requireContext(), ContactDetailsActivity::class.java))
}
override fun displayPluginsButton(): Boolean {
return false
}
@SuppressLint("RestrictedApi")
override fun updateConfInfo(info: List<ParticipantInfo>) {
val binding = binding!!
binding.participantLabelContainer.removeAllViews()
......@@ -576,30 +514,50 @@ class TVCallFragment : BaseSupportFragment<CallPresenter, CallView>(), CallView
binding.confControlGroup!!.visibility = View.GONE
} else {
binding.confControlGroup!!.visibility = View.VISIBLE
if (confAdapter == null) {
confAdapter = ConfParticipantAdapter(object : ConfParticipantSelected {
@SuppressLint("RestrictedApi")
confAdapter?.apply { updateFromCalls(info) }
// Create new adapter
?: ConfParticipantAdapter(info, object : ConfParticipantSelected {
override fun onParticipantSelected(view: View, contact: ParticipantInfo) {
val context = requireContext()
val popup = PopupMenu(context, view)
val maximized = presenter.isMaximized(contact)
val popup = PopupMenu(view.context, view)
popup.inflate(R.menu.conference_participant_actions)
popup.setOnMenuItemClickListener { item: MenuItem ->
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.conv_contact_details -> presenter.openParticipantContact(contact)
R.id.conv_contact_hangup -> presenter.hangupParticipant(contact)
R.id.conv_mute -> presenter.muteParticipant(contact, !contact.audioMuted)
R.id.conv_contact_maximize -> presenter.maximizeParticipant(contact)
else -> return@setOnMenuItemClickListener false
}
true
}
val menuHelper = MenuPopupHelper(context, (popup.menu as MenuBuilder), view)
val menu = popup.menu as MenuBuilder
val maxItem = menu.findItem(R.id.conv_contact_maximize)
val muteItem = menu.findItem(R.id.conv_mute)
if (maximized) {
maxItem.setTitle(R.string.action_call_minimize)
maxItem.setIcon(R.drawable.baseline_close_fullscreen_24)
} else {
maxItem.setTitle(R.string.action_call_maximize)
maxItem.setIcon(R.drawable.baseline_open_in_full_24)
}
if (!contact.audioMuted) {
muteItem.setTitle(R.string.action_call_mute)
muteItem.setIcon(R.drawable.baseline_mic_off_24)
} else {
muteItem.setTitle(R.string.action_call_unmute)
muteItem.setIcon(R.drawable.baseline_mic_24)
}
val menuHelper = MenuPopupHelper(view.context, menu, view)
menuHelper.gravity = Gravity.END
menuHelper.setForceShowIcon(true)
menuHelper.show()
}
})
}
confAdapter!!.updateFromCalls(info)
if (binding.confControlGroup.adapter == null)
binding.confControlGroup.adapter = confAdapter
}).apply {
setHasStableIds(true)
confAdapter = this
binding.confControlGroup.adapter = this
}
}
}
......
......@@ -57,8 +57,7 @@ class TVContactPresenter @Inject constructor(
}
fun contactClicked() {
val account = mAccountService.getAccount(mAccountId!!)
if (account != null) {
mAccountService.getAccount(mAccountId)?.let { account ->
val conversation = account.getByUri(mUri)!!
val conf = conversation.currentCall
val call = conf?.firstCall
......
......@@ -131,7 +131,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:clipToPadding="false"
android:fitsSystemWindows="true">
......
......@@ -38,7 +38,7 @@ along with this program; if not, write to the Free Software
android:layout_margin="@dimen/padding_large"
android:background="@drawable/background_conference_participant"
android:elevation="4dp"
android:ellipsize="end"
android:ellipsize="middle"
android:maxLines="2"
android:paddingStart="16dp"
android:paddingTop="8dp"
......@@ -47,8 +47,10 @@ along with this program; if not, write to the Free Software
android:textAlignment="center"
android:textColor="@color/grey_800"
android:textIsSelectable="false"
android:maxWidth="230dp"
android:scrollHorizontally="false"
android:textSize="16sp"
tools:text="Thomas\nConnecting..." />
tools:text="ring:21d85b6990d2f23663da147db28797ad26c478e421d85b6990d2f23663da147db28797ad26c478e4\nConnecting..." />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/photo"
......@@ -60,7 +62,6 @@ along with this program; if not, write to the Free Software
android:enabled="false"
app:maxImageSize="64dp"
app:useCompatPadding="true"
tools:src="@drawable/ic_contact_picture_fallback" />
</LinearLayout>
......@@ -53,8 +53,8 @@ class CallPresenter @Inject constructor(
@param:Named("UiScheduler") private val mUiScheduler: Scheduler
) : RootPresenter<CallView>() {
private var mConference: Conference? = null
private val mPendingCalls: MutableList<Call> = ArrayList()
private val mPendingSubject: Subject<List<Call>> = BehaviorSubject.createDefault(mPendingCalls)
private val mPendingCalls: MutableList<ParticipantInfo> = ArrayList()
private val mPendingSubject: Subject<List<ParticipantInfo>> = BehaviorSubject.createDefault(mPendingCalls)
private var mOnGoingCall = false
var wantVideo = false
var videoIsMuted = false
......@@ -87,36 +87,14 @@ class CallPresenter @Inject constructor(
}
}
/*override fun unbindView() {
if (wantVideo) {
mHardwareService.endCapture()
}
super.unbindView()
}*/
override fun bindView(view: CallView) {
super.bindView(view)
/*mCompositeDisposable.add(mAccountService.getRegisteredNames()
.observeOn(mUiScheduler)
.subscribe(r -> {
if (mSipCall != null && mSipCall.getContact() != null) {
getView().updateContactBubble(mSipCall.getContact());
}
}));*/
mCompositeDisposable.add(mHardwareService.getVideoEvents()
.observeOn(mUiScheduler)
.subscribe { event: VideoEvent -> onVideoEvent(event) })
mCompositeDisposable.add(mHardwareService.audioState
.observeOn(mUiScheduler)
.subscribe { state: AudioState -> this.view?.updateAudioState(state) })
/*mCompositeDisposable.add(mHardwareService
.getBluetoothEvents()
.subscribe(event -> {
if (!event.connected && mSipCall == null) {
hangupCall();
}
}));*/
}
fun initOutGoing(accountId: String?, conversationUri: Uri?, contactUri: String?, hasVideo: Boolean) {
......@@ -134,14 +112,11 @@ class CallPresenter @Inject constructor(
//getView().blockScreenRotation();
val callObservable = mCallService
.placeCall(accountId, conversationUri, fromString(toNumber(contactUri)!!), pHasVideo)
//.map(mCallService::getConference)
.flatMapObservable { call: Call -> mCallService.getConfUpdates(call) }
.share()
mCompositeDisposable.add(callObservable
.observeOn(mUiScheduler)
.subscribe({ conference: Conference ->
contactUpdate(conference)
confUpdate(conference)
.subscribe({ conference -> confUpdate(conference)
}) { e: Throwable ->
hangupCall()
Log.e(TAG, "Error with initOutgoing: " + e.message, e)
......@@ -166,25 +141,24 @@ class CallPresenter @Inject constructor(
.share()
// Handles the case where the call has been accepted, emits a single so as to only check for permissions and start the call once
mCompositeDisposable.add(callObservable
.firstOrError()
.subscribe({ call: Conference ->
if (!actionViewOnly) {
contactUpdate(call)
if (!actionViewOnly) {
mCompositeDisposable.add(callObservable
.firstOrError()
.subscribe({ call: Conference ->
confUpdate(call)
callInitialized = true
view!!.prepareCall(true)
}
}) { e: Throwable ->
hangupCall()
Log.e(TAG, "Error with initIncoming, preparing call flow :", e)
})
}) { e: Throwable ->
hangupCall()
Log.e(TAG, "Error with initIncoming, preparing call flow :", e)
})
}
// Handles retrieving call updates. Items emitted are only used if call is already in process or if user is returning to a call.
mCompositeDisposable.add(callObservable
.subscribe({ call: Conference ->
if (callInitialized || actionViewOnly) {
contactUpdate(call)
confUpdate(call)
}
}) { e: Throwable ->
......@@ -197,7 +171,13 @@ class CallPresenter @Inject constructor(
private fun showConference(conference: Observable<Conference>) {
val conference = conference.distinctUntilChanged()
mCompositeDisposable.add(conference
.switchMap { obj: Conference -> obj.participantInfo }
.switchMap { obj: Conference -> Observable.combineLatest(obj.participantInfo, mPendingSubject, { participants, pending ->
val p = if (participants.isEmpty() && !obj.isConference)
listOf(ParticipantInfo(obj.call, obj.call!!.contact!!, emptyMap()))
else
participants
if (pending.isEmpty()) p else p + pending
})}
.observeOn(mUiScheduler)
.subscribe({ info: List<ParticipantInfo> -> view?.updateConfInfo(info) })
{ e: Throwable -> Log.e(TAG, "Error with initIncoming, action view flow: ", e) })
......@@ -222,10 +202,9 @@ class CallPresenter @Inject constructor(
}
fun chatClick() {
if (mConference == null || mConference!!.participants.isEmpty()) {
return
}
val firstCall = mConference!!.participants[0] ?: return
val conference = mConference ?: return
if (conference.participants.isEmpty()) return
val firstCall = conference.participants[0]
val c = firstCall.conversation
if (c is Conversation) {
view?.goToConversation(c.accountId, c.uri)
......@@ -281,7 +260,7 @@ class CallPresenter @Inject constructor(
mCallService.hangUp(conference.id)
}
for (call in mPendingCalls) {
mCallService.hangUp(call.daemonIdString!!)
mCallService.hangUp(call.call!!.daemonIdString!!)
}
finish()
}
......@@ -292,17 +271,15 @@ class CallPresenter @Inject constructor(
}
fun videoSurfaceCreated(holder: Any) {
if (mConference == null) {
return
}
val newId = mConference!!.id
val conference = mConference ?: return
val newId = conference.id
if (newId != currentSurfaceId) {
currentSurfaceId?.let { id ->
mHardwareService.removeVideoSurface(id)
}
currentSurfaceId = newId
}
mHardwareService.addVideoSurface(mConference!!.id, holder)
mHardwareService.addVideoSurface(conference.id, holder)
view?.displayContactBubble(false)
}
......@@ -316,17 +293,15 @@ class CallPresenter @Inject constructor(
}
fun pluginSurfaceCreated(holder: Any) {
if (mConference == null) {
return
}
val newId = mConference!!.pluginId
val conference = mConference ?: return
val newId = conference.pluginId
if (newId != currentPluginSurfaceId) {
currentPluginSurfaceId?.let { id ->
mHardwareService.removeVideoSurface(id)
}
currentPluginSurfaceId = newId
}
mHardwareService.addVideoSurface(mConference!!.pluginId, holder)
mHardwareService.addVideoSurface(conference.pluginId, holder)
view?.displayContactBubble(false)
}
......@@ -363,14 +338,6 @@ class CallPresenter @Inject constructor(
mHardwareService.endCapture()
}
fun displayChanged() {
mHardwareService.switchInput(mConference!!.id, false)
}
fun layoutChanged() {
//getView().resetVideoSize(videoWidth, videoHeight, previewWidth, previewHeight);
}
fun uiVisibilityChanged(displayed: Boolean) {
Log.w(TAG, "uiVisibilityChanged $mOnGoingCall $displayed")
view?.displayHangupButton(mOnGoingCall && displayed)
......@@ -387,51 +354,8 @@ class CallPresenter @Inject constructor(
view?.finish()
}
private var contactDisposable: Disposable? = null
private fun contactUpdate(conference: Conference) {
if (mConference !== conference) {
mConference = conference
contactDisposable?.apply { dispose() }
if (conference.participants.isEmpty()) return
// Updates of participant (and pending participant) list
val callsObservable = mPendingSubject
.map<List<Call>> { pendingList: List<Call> ->
Log.w(TAG, "mPendingSubject onNext " + pendingList.size + " " + conference.participants.size)
if (pendingList.isEmpty()) return@map conference.participants
val newList: MutableList<Call> = ArrayList(conference.participants.size + pendingList.size)
newList.addAll(conference.participants)
newList.addAll(pendingList)
newList
}
// Updates of individual contacts
val contactsObservable = callsObservable.flatMapSingle { calls: List<Call> ->
Observable.fromIterable(calls)
.map { call: Call -> mContactService.observeContact(call.account!!, call.contact!!, false)
.map { call } }
.toList(calls.size)
}
// Combined updates of contacts as participant list updates
val contactUpdates = contactsObservable
.switchMap { list: List<Observable<Call>> -> Observable.combineLatest(list) { objects: Array<Any> ->
Log.w(TAG, "flatMapObservable " + objects.size)
val calls = ArrayList<Call>(objects.size)
for (call in objects) calls.add(call as Call)
calls
} }
.filter { list: List<Call> -> list.isNotEmpty() }
contactDisposable = contactUpdates
.observeOn(mUiScheduler)
.subscribe({ cs: List<Call> -> view?.updateContactBubble(cs) })
{ e: Throwable -> Log.e(TAG, "Error updating contact data", e) }
.apply { mCompositeDisposable.add(this) }
}
mPendingSubject.onNext(mPendingCalls)
}
private fun confUpdate(call: Conference) {
mConference = call
Log.w(TAG, "confUpdate " + call.id + " " + call.state)
val status = call.state
if (status === CallStatus.HOLD) {
......@@ -482,27 +406,26 @@ class CallPresenter @Inject constructor(
}
fun maximizeParticipant(info: ParticipantInfo?) {
var info = info
val conference = mConference ?: return
val contact = info?.contact
if (mConference!!.maximizedParticipant == contact) info = null
mConference!!.maximizedParticipant = contact
if (info != null) {