Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ConversationFragment.kt 53.13 KiB
/*
 *  Copyright (C) 2004-2022 Savoir-faire Linux Inc.
 *
 *  Author: 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.LayoutTransition
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityOptions
import android.content.*
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.pm.PackageManager
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.IBinder
import android.provider.MediaStore
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.util.Log
import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.*
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuPopupHelper
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.core.view.*
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import cx.ring.R
import cx.ring.adapters.ConversationAdapter
import cx.ring.client.CallActivity
import cx.ring.client.ContactDetailsActivity
import cx.ring.client.ConversationActivity
import cx.ring.client.HomeActivity
import cx.ring.databinding.FragConversationBinding
import cx.ring.interfaces.Colorable
import cx.ring.mvp.BaseSupportFragment
import cx.ring.service.DRingService
import cx.ring.service.LocationSharingService
import cx.ring.services.NotificationServiceImpl
import cx.ring.services.SharedPreferencesServiceImpl.Companion.getConversationPreferences
import cx.ring.utils.*
import cx.ring.utils.MediaButtonsHelper.MediaButtonsHelperCallback
import cx.ring.views.AvatarDrawable
import cx.ring.views.AvatarFactory
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import net.jami.conversation.ConversationPresenter
import net.jami.conversation.ConversationView
import net.jami.daemon.JamiService
import net.jami.model.*
import net.jami.model.Account.ComposingStatus
import net.jami.services.NotificationService
import net.jami.smartlist.ConversationItemViewModel
import java.io.File
import java.util.*

@AndroidEntryPoint
class ConversationFragment : BaseSupportFragment<ConversationPresenter, ConversationView>(),
    MediaButtonsHelperCallback, ConversationView, OnSharedPreferenceChangeListener,
    SearchView.OnQueryTextListener {
    private var locationServiceConnection: ServiceConnection? = null
    private var binding: FragConversationBinding? = null
    private var mAudioCallBtn: MenuItem? = null
    private var mVideoCallBtn: MenuItem? = null
    private var currentBottomView: View? = null
    private var mAdapter: ConversationAdapter? = null
    private var mSearchAdapter: ConversationAdapter? = null
    private var marginPx = 0
    private var marginPxTotal = 0
    private val animation = ValueAnimator()
    private var mPreferences: SharedPreferences? = null
    private var mCurrentPhoto: File? = null
    private var mCurrentFileAbsolutePath: String? = null
    private val mCompositeDisposable = CompositeDisposable()
    private var mSelectedPosition = 0
    private var replyingTo: Interaction? = null
    private var mIsBubble = false
    private var mConversationAvatar: AvatarDrawable? = null
    private val mParticipantAvatars: MutableMap<String, AvatarDrawable> = HashMap()
    private val mSmallParticipantAvatars: MutableMap<String, AvatarDrawable> = HashMap()
    private var mapWidth = 0
    private var mapHeight = 0
    private var loading = true
    private var animating = 0

    private val pickMultipleMedia =
        registerForActivityResult(ActivityResultContracts.PickMultipleVisualMedia(8)) { uris ->
            for (uri in uris) {
                startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri)
                    .observeOn(DeviceUtils.uiScheduler)
                    .flatMapCompletable { file: File -> sendFile(file) })
            }
        }

    fun getConversationAvatar(uri: String): AvatarDrawable? = mParticipantAvatars[uri]

    fun getSmallConversationAvatar(uri: String): AvatarDrawable? {
        synchronized(mSmallParticipantAvatars) { return mSmallParticipantAvatars[uri] }
    }

    override fun refreshView(conversation: List<Interaction>) {
        if (binding != null) binding!!.pbLoading.visibility = View.GONE
        mAdapter?.let { adapter ->
            adapter.updateDataset(conversation)
            loading = false
        }
        requireActivity().invalidateOptionsMenu()
    }

    override fun scrollToEnd() {
        mAdapter?.let { adapter ->
            if (adapter.itemCount > 0)
                binding!!.histList.scrollToPosition(adapter.itemCount - 1)
        }
    }

    override fun scrollToMessage(messageId: String, highlight: Boolean) {
        val histList = binding?.histList ?: return
        mAdapter?.let { adapter ->
            val position = adapter.getMessagePosition(messageId)
            if(position == -1)
                return
            binding!!.histList.scrollToPosition(position)

            if(highlight) {
                histList.doOnNextLayout {
                    histList.layoutManager?.findViewByPosition(position)
                        ?.setBackgroundColor(resources.getColor(R.color.surface))
                }
            }
        }
    }

    private fun updateListPadding() {
        /* val binding = binding ?: return
        val bottomView = currentBottomView ?: return
        val bottomViewHeight = bottomView.height
        if (bottomViewHeight != 0) {
            val padding = bottomViewHeight + marginPxTotal
            val params = binding.mapCard.layoutParams as RelativeLayout.LayoutParams
            params.bottomMargin = padding
            binding.mapCard.layoutParams = params
        } */
    }

    override fun displayErrorToast(error: Error) {
        val errorString: String = when (error) {
            Error.NO_INPUT -> getString(R.string.call_error_no_camera_no_microphone)
            Error.INVALID_FILE -> getString(R.string.invalid_file)
            Error.NOT_ABLE_TO_WRITE_FILE -> getString(R.string.not_able_to_write_file)
            Error.NO_SPACE_LEFT -> getString(R.string.no_space_left_on_device)
            else -> getString(R.string.generic_error)
        }
        Toast.makeText(requireContext(), errorString, Toast.LENGTH_LONG).show()
    }

    private val onBackPressedCallback: OnBackPressedCallback =
        object : OnBackPressedCallback(false) {
            override fun handleOnBackPressed() {
                val count = childFragmentManager.backStackEntryCount
                if (count > 0) {
                    childFragmentManager.popBackStack()
                    if (count == 1)
                        isEnabled = false
                }
            }
        }

    override fun onAttach(context: Context) {
        super.onAttach(context)
        activity?.onBackPressedDispatcher?.addCallback(this, onBackPressedCallback)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val res = resources
        marginPx = res.getDimensionPixelSize(R.dimen.conversation_message_input_margin)
        mapWidth = res.getDimensionPixelSize(R.dimen.location_sharing_minmap_width)
        mapHeight = res.getDimensionPixelSize(R.dimen.location_sharing_minmap_height)
        marginPxTotal = marginPx
        return FragConversationBinding.inflate(inflater, container, false).let { binding ->
            this@ConversationFragment.binding = binding
            binding.presenter = this@ConversationFragment
            animation.duration = 150
            animation.addUpdateListener { valueAnimator: ValueAnimator -> binding.histList.updatePadding(bottom = valueAnimator.animatedValue as Int) }

            (activity as AppCompatActivity?)!!.setSupportActionBar(binding.toolbar)

            val layoutToAnimate = binding.relativeLayout
            if (Build.VERSION.SDK_INT >= 30) {
                ViewCompat.setWindowInsetsAnimationCallback(
                    layoutToAnimate,
                    object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
                        override fun onPrepare(animation: WindowInsetsAnimationCompat) {
                            animating++
                        }
                        override fun onProgress(insets: WindowInsetsCompat, runningAnimations: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
                            layoutToAnimate.updatePadding(bottom = insets.systemWindowInsetBottom)
                            return insets
                        }

                        override fun onEnd(animation: WindowInsetsAnimationCompat) {
                            animating--
                        }
                    })
            }
            ViewCompat.setOnApplyWindowInsetsListener(layoutToAnimate) { _, insets: WindowInsetsCompat ->
                if (animating == 0) {
                    layoutToAnimate.updatePadding(
                        top = insets.systemWindowInsetTop,
                        bottom = insets.systemWindowInsetBottom
                    )
                }
                WindowInsetsCompat.CONSUMED
            }

            binding.ongoingcallPane.visibility = View.GONE
            ViewCompat.setOnReceiveContentListener(binding.msgInputTxt, SUPPORTED_MIME_TYPES) { _, contentInfo ->
                for (i in 0 until contentInfo.clip.itemCount) {
                    val item: ClipData.Item = contentInfo.clip.getItemAt(i)
                    if (item.uri == null && item.text != null) {
                        binding.msgInputTxt.setText(item.text)
                    } else {
                        startFileSend(AndroidFileUtils.getCacheFile(requireContext(), item.uri)
                            .flatMapCompletable { sendFile(it) })
                    }
                }
                null
            }
            binding.msgInputTxt.setOnEditorActionListener { _, actionId: Int, _ -> actionSendMsgText(actionId) }
            binding.msgInputTxt.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus: Boolean ->
                if (hasFocus) {
                    (childFragmentManager.findFragmentById(R.id.mapLayout) as LocationSharingFragment?)?.hideControls()
                }
            }
            binding.msgInputTxt.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) {}
                override fun afterTextChanged(s: Editable) {
                    val message = s.toString()
                    val hasMessage = !TextUtils.isEmpty(message)
                    presenter.onComposingChanged(hasMessage)
                    if (hasMessage) {
                        binding.msgSend.visibility = View.VISIBLE
                        binding.emojiSend.visibility = View.GONE
                    } else {
                        binding.msgSend.visibility = View.GONE
                        binding.emojiSend.visibility = View.VISIBLE
                    }
                    mPreferences?.let { preferences ->
                        if (hasMessage)
                            preferences.edit().putString(KEY_PREFERENCE_PENDING_MESSAGE, message).apply()
                        else
                            preferences.edit().remove(KEY_PREFERENCE_PENDING_MESSAGE).apply()
                    }
                }
            })
            binding.replyCloseBtn.setOnClickListener {
                clearReply()
            }
            binding.fabLatest.setOnClickListener {
                scrollToEnd()
            }
            setHasOptionsMenu(true)
            if (!DeviceUtils.isTablet(requireContext())) {
                binding.toolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material)
                binding.toolbar.setNavigationOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() }
            }
            binding.root
        }
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding?.let { binding ->
            mPreferences?.let { preferences ->
                val pendingMessage = preferences.getString(KEY_PREFERENCE_PENDING_MESSAGE, null)
                if (!pendingMessage.isNullOrEmpty()) {
                    binding.msgInputTxt.setText(pendingMessage)
                    binding.msgSend.visibility = View.VISIBLE
                    binding.emojiSend.visibility = View.GONE
                }
            }
            binding.msgInputTxt.addOnLayoutChangeListener { _, _, _, _, _, oldLeft, oldTop, oldRight, oldBottom ->
                if (oldBottom == 0 && oldTop == 0) {
                    updateListPadding()
                } else {
                    if (animation.isStarted) animation.cancel()
                    animation.setIntValues(
                        binding.histList.paddingBottom,
                        (currentBottomView?.height ?: 0) + marginPxTotal
                    )
                    animation.start()
                }
            }
            binding.histList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
                // The minimum amount of items to have below current scroll position
                // before loading more.
                val visibleLoadThreshold = 3
                // The amount of items to have below the current scroll position to display
                // the scroll to latest button.
                val visibleLatestThreshold = 8
                override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {}
                override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                    val layoutManager = recyclerView.layoutManager as LinearLayoutManager?
                    if (!loading && layoutManager!!.findFirstVisibleItemPosition() < visibleLoadThreshold) {
                        loading = true
                        presenter.loadMore()
                    }
                    if (layoutManager!!.itemCount - layoutManager.findLastVisibleItemPosition() > visibleLatestThreshold) {
                        binding.fabLatest.show()
                    } else {
                        binding.fabLatest.hide()
                    }
                }
            })
            val animator = binding.histList.itemAnimator as DefaultItemAnimator?
            animator?.supportsChangeAnimations = false
            binding.histList.adapter = mAdapter

//            val toolbarLayout: AppBarLayout? = activity?.findViewById(R.id.toolbar_layout)
//            toolbarLayout?.isLifted = true
        }
    }

    override fun setConversationColor(@ColorInt color: Int) {
        val activity = activity as Colorable?
        activity?.setColor(color)
        mAdapter?.setPrimaryColor(color)
    }

    override fun setConversationSymbol(symbol: CharSequence) {
        binding?.emojiSend?.text = symbol
    }

    override fun onDestroyView() {
        mPreferences?.unregisterOnSharedPreferenceChangeListener(this)
        animation.removeAllUpdateListeners()
        binding?.histList?.adapter = null
        mCompositeDisposable.clear()
        locationServiceConnection?.let {
            try {
                requireContext().unbindService(it)
            } catch (e: Exception) {
                Log.w(TAG, "Error unbinding service: " + e.message)
            }
        }
        mAdapter = null
        super.onDestroyView()
        binding = null
    }

    override fun onContextItemSelected(item: MenuItem): Boolean =
        if (mAdapter!!.onContextItemSelected(item)) true
        else super.onContextItemSelected(item)

    fun updateAdapterItem() {
        if (mSelectedPosition != -1) {
            mAdapter?.notifyItemChanged(mSelectedPosition)
            mSelectedPosition = -1
        }
    }

    private fun clearReply() {
        if (replyingTo != null) {
            replyingTo = null
            binding?.apply {
                replyGroup.isVisible = false
            }
        }
    }

    fun sendMessageText() {
        val message = binding!!.msgInputTxt.text.toString()
        presenter.sendTextMessage(message, replyingTo)
        clearMsgEdit()
    }

    fun sendEmoji() {
        presenter.sendTextMessage(binding!!.emojiSend.text.toString(), replyingTo)
        clearReply()
    }

    @SuppressLint("RestrictedApi")
    fun expandMenu(v: View) {
        val context = requireContext()
        val popup = PopupMenu(context, v)
        popup.inflate(R.menu.conversation_share_actions)
        popup.setOnMenuItemClickListener { item: MenuItem ->
            when (item.itemId) {
                R.id.conv_send_audio -> sendAudioMessage()
                R.id.conv_send_video -> sendVideoMessage()
                R.id.conv_send_file -> openFilePicker()
                R.id.conv_select_media -> openGallery()
                R.id.conv_share_location -> shareLocation()
                R.id.chat_plugins -> presenter.showPluginListHandlers()
            }
            false
        }
        popup.menu.findItem(R.id.chat_plugins).isVisible = JamiService.getPluginsEnabled() && !JamiService.getChatHandlers().isEmpty()
        val menuHelper = MenuPopupHelper(context, (popup.menu as MenuBuilder), v)
        menuHelper.setForceShowIcon(true)
        menuHelper.show()
    }

    override fun showPluginListHandlers(accountId: String, contactId: String) {
        Log.w(TAG, "show Plugin Chat Handlers List")
        val fragment = PluginHandlersListFragment.newInstance(accountId, contactId)
        childFragmentManager.beginTransaction()
            .add(R.id.pluginListHandlers, fragment, PluginHandlersListFragment.TAG)
            .commit()
        binding?.let { binding ->
            val params = binding.mapCard.layoutParams as RelativeLayout.LayoutParams
            if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
                params.width = ViewGroup.LayoutParams.MATCH_PARENT
                params.height = ViewGroup.LayoutParams.MATCH_PARENT
                binding.mapCard.layoutParams = params
            }
            binding.mapCard.visibility = View.VISIBLE
        }
    }

    fun hidePluginListHandlers() {
        if (binding!!.mapCard.visibility != View.GONE) {
            binding!!.mapCard.visibility = View.GONE
            val fragmentManager = childFragmentManager
            val fragment = fragmentManager.findFragmentById(R.id.pluginListHandlers)
            if (fragment != null) {
                fragmentManager.beginTransaction()
                    .remove(fragment)
                    .commit()
            }
        }
        val params = binding!!.mapCard.layoutParams as RelativeLayout.LayoutParams
        if (params.width != mapWidth) {
            params.width = mapWidth
            params.height = mapHeight
            binding!!.mapCard.layoutParams = params
        }
    }

    private fun shareLocation() {
        presenter.shareLocation()
    }

    fun closeLocationSharing(isSharing: Boolean) {
        val params = binding!!.mapCard.layoutParams as RelativeLayout.LayoutParams
        if (params.width != mapWidth) {
            params.width = mapWidth
            params.height = mapHeight
            binding!!.mapCard.layoutParams = params
        }
        if (!isSharing) hideMap()
    }

    fun openLocationSharing() {
        binding!!.conversationLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
        val params = binding!!.mapCard.layoutParams as RelativeLayout.LayoutParams
        if (params.width != ViewGroup.LayoutParams.MATCH_PARENT) {
            params.width = ViewGroup.LayoutParams.MATCH_PARENT
            params.height = ViewGroup.LayoutParams.MATCH_PARENT
            binding!!.mapCard.layoutParams = params
        }
    }

    override fun startShareLocation(accountId: String, conversationId: String) {
        showMap(accountId, conversationId, true)
    }

    /**
     * Used to update with the past adapter position when a long click was registered
     */
    fun updatePosition(position: Int) {
        mSelectedPosition = position
    }

    override fun showMap(accountId: String, contactId: String, open: Boolean) {
        if (binding!!.mapCard.visibility == View.GONE) {
            Log.w(TAG, "showMap $accountId $contactId")
            val fragmentManager = childFragmentManager
            val fragment = LocationSharingFragment.newInstance(accountId, contactId, open)
            fragmentManager.beginTransaction()
                .add(R.id.mapLayout, fragment, "map")
                .commit()
            binding!!.mapCard.visibility = View.VISIBLE
        }
        if (open) {
            val fragment = childFragmentManager.findFragmentById(R.id.mapLayout)
            if (fragment != null) {
                (fragment as LocationSharingFragment).showControls()
            }
        }
    }

    override fun hideMap() {
        if (binding!!.mapCard.visibility != View.GONE) {
            binding!!.mapCard.visibility = View.GONE
            val fragmentManager = childFragmentManager
            val fragment = fragmentManager.findFragmentById(R.id.mapLayout)
            if (fragment != null) {
                fragmentManager.beginTransaction()
                    .remove(fragment)
                    .commit()
            }
        }
    }

    private fun sendAudioMessage() {
        if (!presenter.deviceRuntimeService.hasAudioPermission()) {
            requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), REQUEST_CODE_CAPTURE_AUDIO)
        } else {
            try {
                val ctx = requireContext()
                val intent = Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION)
                mCurrentPhoto = AndroidFileUtils.createAudioFile(ctx)
                startActivityForResult(intent, REQUEST_CODE_CAPTURE_AUDIO)
            } catch (ex: Exception) {
                Log.e(TAG, "sendAudioMessage: error", ex)
                Toast.makeText(activity, "Can't find audio recorder app", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun sendVideoMessage() {
        if (!presenter.deviceRuntimeService.hasVideoPermission()) {
            requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CODE_CAPTURE_VIDEO)
        } else {
            try {
                val context = requireContext()
                val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply {
                    putExtra("android.intent.extras.CAMERA_FACING", 1)
                    putExtra("android.intent.extras.LENS_FACING_FRONT", 1)
                    putExtra("android.intent.extra.USE_FRONT_CAMERA", true)
                    putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0)
                    putExtra(MediaStore.EXTRA_OUTPUT, ContentUriHandler.getUriForFile(context, ContentUriHandler.AUTHORITY_FILES, AndroidFileUtils.createVideoFile(context).apply {
                        mCurrentPhoto = this
                    }))
                }
                startActivityForResult(intent, REQUEST_CODE_CAPTURE_VIDEO)
            } catch (ex: Exception) {
                Log.e(TAG, "sendVideoMessage: error", ex)
                Toast.makeText(activity, "Can't find video recorder app", Toast.LENGTH_SHORT).show()
            }
        }
    }

    fun takePicture() {
        if (!presenter.deviceRuntimeService.hasVideoPermission()) {
            requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CODE_TAKE_PICTURE)
            return
        }
        val c = context ?: return
        try {
            val photoFile = AndroidFileUtils.createImageFile(c)
            Log.i(TAG, "takePicture: trying to save to $photoFile")
            val photoURI = ContentUriHandler.getUriForFile(c, ContentUriHandler.AUTHORITY_FILES, photoFile)
            val takePictureIntent =
                Intent(MediaStore.ACTION_IMAGE_CAPTURE).putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                    .putExtra("android.intent.extras.CAMERA_FACING", 1)
                    .putExtra("android.intent.extras.LENS_FACING_FRONT", 1)
                    .putExtra("android.intent.extra.USE_FRONT_CAMERA", true)
            mCurrentPhoto = photoFile
            startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PICTURE)
        } catch (e: Exception) {
            Toast.makeText(c, "Error taking picture: " + e.localizedMessage, Toast.LENGTH_SHORT)
                .show()
        }
    }

    override fun openFilePicker() {
        val intent = Intent(Intent.ACTION_GET_CONTENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
        intent.type = "*/*"
        startActivityForResult(intent, REQUEST_CODE_FILE_PICKER)
    }

    private fun openGallery() {
        pickMultipleMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo))
    }

    private fun sendFile(file: File): Completable = Completable.fromAction {
        presenter.sendFile(file)
    }

    private fun startFileSend(op: Completable): Disposable {
        setLoading(true)
        return op.observeOn(DeviceUtils.uiScheduler)
            .doFinally { setLoading(false) }
            .subscribe({}) { e ->
                Log.e(TAG, "startFileSend: not able to create cache file", e)
                displayErrorToast(Error.INVALID_FILE)
            }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
        Log.w(TAG, "onActivityResult: $requestCode $resultCode $resultData")
        if (requestCode == REQUEST_CODE_FILE_PICKER) {
            if (resultCode == Activity.RESULT_OK && resultData != null) {
                val clipData = resultData.clipData
                if (clipData != null) { // checking multiple selection or not
                    for (i in 0 until clipData.itemCount) {
                        val uri = clipData.getItemAt(i).uri
                        startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri)
                            .observeOn(DeviceUtils.uiScheduler)
                            .flatMapCompletable { file: File -> sendFile(file) })
                    }
                } else {
                    resultData.data?.let { uri ->
                        startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri)
                            .observeOn(DeviceUtils.uiScheduler)
                            .flatMapCompletable { file: File -> sendFile(file) })
                    }
                }
            }
        } else if (requestCode == REQUEST_CODE_TAKE_PICTURE || requestCode == REQUEST_CODE_CAPTURE_AUDIO || requestCode == REQUEST_CODE_CAPTURE_VIDEO) {
            if (resultCode != Activity.RESULT_OK) {
                mCurrentPhoto = null
                return
            }
            val currentPhoto = mCurrentPhoto
            var file: Single<File>? = null
            if (currentPhoto == null || !currentPhoto.exists() || currentPhoto.length() == 0L) {
                resultData?.data?.let { uri ->
                    file = AndroidFileUtils.getCacheFile(requireContext(), uri)
                }
            } else {
                file = Single.just(currentPhoto)
            }
            mCurrentPhoto = null
            val sendingFile = file
            if (sendingFile != null)
                startFileSend(sendingFile.flatMapCompletable { f -> sendFile(f) })
            else
                Toast.makeText(activity, "Can't find picture", Toast.LENGTH_SHORT).show()
        } else if (requestCode == REQUEST_CODE_SAVE_FILE) {
            val uri = resultData?.data
            if (resultCode == Activity.RESULT_OK && uri != null) {
                writeToFile(uri)
            }
        } else if (requestCode == REQUEST_CODE_EDIT_MESSAGE) {
            val uri = resultData?.data ?: return
            if (resultCode == Activity.RESULT_OK) {
                val path = InteractionPath.fromUri(uri) ?: return
                val message = resultData.getStringExtra(Intent.EXTRA_TEXT) ?: return
                presenter.editMessage(path.conversation.accountId, path.conversation.conversationUri, path.messageId, message)
            }
        }
    }

    private fun writeToFile(data: Uri) {
        val path = mCurrentFileAbsolutePath ?: return
        val cr = context?.contentResolver ?: return
        mCompositeDisposable.add(AndroidFileUtils.copyFileToUri(cr, File(path), data)
            .observeOn(DeviceUtils.uiScheduler)
            .subscribe({ Toast.makeText(context, R.string.file_saved_successfully, Toast.LENGTH_SHORT).show() })
            { Toast.makeText(context, R.string.generic_error, Toast.LENGTH_SHORT).show() })
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        for (i in permissions.indices) {
            val granted = grantResults[i] == PackageManager.PERMISSION_GRANTED
            when (permissions[i]) {
                Manifest.permission.CAMERA -> {
                    presenter.cameraPermissionChanged(granted)
                    if (granted) {
                        if (requestCode == REQUEST_CODE_CAPTURE_VIDEO)
                            sendVideoMessage()
                        else if (requestCode == REQUEST_CODE_TAKE_PICTURE)
                            takePicture()
                    }
                    return
                }
                Manifest.permission.RECORD_AUDIO -> {
                    if (granted && requestCode == REQUEST_CODE_CAPTURE_AUDIO)
                        sendAudioMessage()
                    return
                }
                else -> {}
            }
        }
    }

    override fun addElement(element: Interaction) {
        if (mAdapter!!.add(element) && element.type != Interaction.InteractionType.INVALID)
            scrollToEnd()
        loading = false
    }

    override fun updateElement(element: Interaction) {
        mAdapter?.update(element)
    }

    override fun removeElement(element: Interaction) {
        mAdapter?.remove(element)
    }

    override fun setComposingStatus(composingStatus: ComposingStatus) {
        mAdapter?.setComposingStatus(composingStatus)
        if (composingStatus == ComposingStatus.Active) scrollToEnd()
    }

    override fun acceptFile(accountId: String, conversationUri: net.jami.model.Uri, transfer: DataTransfer) {
        if (transfer.messageId == null && transfer.fileId == null)
            return
        val cacheDir = requireContext().cacheDir
        val spaceLeft = AndroidFileUtils.getSpaceLeft(cacheDir.toString())
        if (spaceLeft == -1L || transfer.totalSize > spaceLeft) {
            presenter.noSpaceLeft()
            return
        }
        requireActivity().startService(Intent(DRingService.ACTION_FILE_ACCEPT, ConversationPath.toUri(accountId, conversationUri),
            requireContext(), DRingService::class.java)
            .putExtra(DRingService.KEY_MESSAGE_ID, transfer.messageId)
            .putExtra(DRingService.KEY_TRANSFER_ID, transfer.fileId)
        )
    }

    override fun refuseFile(accountId: String, conversationUri: net.jami.model.Uri, transfer: DataTransfer) {
        if (transfer.messageId == null && transfer.fileId == null)
            return
        requireActivity().startService(Intent(DRingService.ACTION_FILE_CANCEL, ConversationPath.toUri(accountId, conversationUri),
            requireContext(), DRingService::class.java)
            .putExtra(DRingService.KEY_MESSAGE_ID, transfer.messageId)
            .putExtra(DRingService.KEY_TRANSFER_ID, transfer.fileId)
        )
    }

    override fun shareFile(path: File, displayName: String) {
        val c = context ?: return
        AndroidFileUtils.shareFile(c, path, displayName)
    }

    override fun openFile(path: File, displayName: String) {
        val c = context ?: return
        AndroidFileUtils.openFile(c, path, displayName)
    }
    private fun actionSendMsgText(actionId: Int): Boolean = when (actionId) {
        EditorInfo.IME_ACTION_SEND -> {
            sendMessageText()
            true
        }
        else -> false
    }

    fun onClick() {
        presenter.clickOnGoingPane()
    }

    override fun onStart() {
        super.onStart()
        presenter.resume(mIsBubble)
    }

    override fun onStop() {
        super.onStop()
        presenter.pause()
    }

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

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        if (!isVisible)
            return
        menu.clear()
        inflater.inflate(R.menu.conversation_actions, menu)
        mAudioCallBtn = menu.findItem(R.id.conv_action_audiocall)
        mVideoCallBtn = menu.findItem(R.id.conv_action_videocall)
        val searchMenuItem = menu.findItem(R.id.conv_search)
        searchMenuItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
            override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
                val binding = binding ?: return false
                presenter.stopSearch()
                binding.histList.adapter = mAdapter
                updateListPadding()
                currentBottomView?.isVisible = true
                if (animation.isStarted) animation.cancel()
                animation.setIntValues(binding.histList.paddingBottom, currentBottomView!!.height + marginPxTotal)
                animation.start()
                return true
            }
            override fun onMenuItemActionExpand(item: MenuItem): Boolean {
                val binding = binding ?: return false
                mSearchAdapter = ConversationAdapter(this@ConversationFragment, presenter, true)
                presenter.startSearch()
                currentBottomView?.isVisible = false
                binding.histList.adapter = mSearchAdapter
                if (animation.isStarted) animation.cancel()
                animation.setIntValues(binding.histList.paddingBottom, marginPxTotal)
                animation.start()
                return true
            }
        })
        (searchMenuItem.actionView as SearchView).let {
            it.setOnQueryTextListener(this)
            it.queryHint = getString(R.string.conversation_search_hint)
        }
    }

    fun openContact() {
        presenter.openContact()
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            android.R.id.home -> startActivity(Intent(activity, HomeActivity::class.java))
            R.id.conv_action_audiocall -> presenter.goToCall(false)
            R.id.conv_action_videocall -> presenter.goToCall(true)
            R.id.conv_contact_details -> presenter.openContact()
            else -> return super.onOptionsItemSelected(item)
        }
        return true
    }

    override fun onQueryTextSubmit(query: String): Boolean {
        return true
    }

    override fun onQueryTextChange(query: String): Boolean {
        if (query.isNotBlank())
            presenter.setSearchQuery(query.trim())
        mSearchAdapter?.clearSearchResults()
        return true
    }

    override fun addSearchResults(results: List<Interaction>) {
        mSearchAdapter?.addSearchResults(results)
    }

    override fun shareText(body: String) {
        startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply {
            putExtra(Intent.EXTRA_TEXT, body)
            type = "text/plain"
        }, null))
    }

    override fun initPresenter(presenter: ConversationPresenter) {
        val path = ConversationPath.fromBundle(arguments)
        mIsBubble = requireArguments().getBoolean(NotificationServiceImpl.EXTRA_BUBBLE)
        Log.w(TAG, "initPresenter $path")
        if (path == null) return
        val uri = path.conversationUri
        mAdapter = ConversationAdapter(this, presenter)
        presenter.init(uri, path.accountId)
        try {
            mPreferences = getConversationPreferences(requireContext(), path.accountId, uri).also { preferences ->
                preferences.registerOnSharedPreferenceChangeListener(this)
                presenter.setConversationColor(preferences.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, resources.getColor(R.color.color_primary_light)))
                presenter.setConversationSymbol(preferences.getString(KEY_PREFERENCE_CONVERSATION_SYMBOL, resources.getText(R.string.conversation_default_emoji).toString())!!)
                preferences.edit().remove(KEY_PREFERENCE_CONVERSATION_LAST_READ).apply()
            }
        } catch (e: Exception) {
            Log.e(TAG, "Can't load conversation preferences")
        }
        var connection = locationServiceConnection
        if (connection == null) {
            connection = object : ServiceConnection {
                override fun onServiceConnected(name: ComponentName, service: IBinder) {
                    Log.w(TAG, "onServiceConnected")
                    val binder = service as LocationSharingService.LocalBinder
                    val locationService = binder.service
                    //val path = ConversationPath(presenter.path)
                    if (locationService.isSharing(path)) {
                        showMap(path.accountId, uri.uri, false)
                    }
                    /*try {
                        requireContext().unbindService(locationServiceConnection!!)
                    } catch (e: Exception) {
                        Log.w(TAG, "Error unbinding service", e)
                    }*/
                }

                override fun onServiceDisconnected(name: ComponentName) {
                    Log.w(TAG, "onServiceDisconnected")
                    locationServiceConnection = null
                }
            }
            locationServiceConnection = connection
            Log.w(TAG, "bindService")
            requireContext().bindService(Intent(requireContext(), LocationSharingService::class.java), connection, 0)
        }
    }

    override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
        when (key) {
            KEY_PREFERENCE_CONVERSATION_COLOR -> presenter.setConversationColor(
                prefs.getInt(KEY_PREFERENCE_CONVERSATION_COLOR, resources.getColor(R.color.color_primary_light)))
            KEY_PREFERENCE_CONVERSATION_SYMBOL -> presenter.setConversationSymbol(
                prefs.getString(KEY_PREFERENCE_CONVERSATION_SYMBOL, resources.getText(R.string.conversation_default_emoji).toString())!!)
        }
    }

    override fun updateContact(contact: ContactViewModel) {
        val contactKey = contact.contact.primaryNumber
        val a = mSmallParticipantAvatars[contactKey]
        if (a != null) {
            a.update(contact)
            mParticipantAvatars[contactKey]!!.update(contact)
            mAdapter?.setPhoto()
        } else {
            val builder = AvatarDrawable.Builder()
                .withContact(contact)
                .withCircleCrop(true)
            mParticipantAvatars[contactKey] = builder
                .withPresence(true)
                .build(requireContext())
            mSmallParticipantAvatars[contactKey] = builder
                .withPresence(false)
                .build(requireContext())
        }
    }

    override fun displayContact(conversation: ConversationItemViewModel) {
        val avatar = AvatarFactory.getAvatar(requireContext(), conversation).blockingGet()
        mConversationAvatar = avatar
        mParticipantAvatars[conversation.uri.rawRingId] = AvatarDrawable(avatar)
        setupActionbar(conversation)
    }

    override fun displayOnGoingCallPane(display: Boolean) {
        binding!!.ongoingcallPane.visibility = if (display) View.VISIBLE else View.GONE
    }

    override fun displayNumberSpinner(conversation: Conversation, number: net.jami.model.Uri) {
        binding!!.numberSelector.visibility = View.VISIBLE
        //binding.numberSelector.setAdapter(new NumberAdapter(getActivity(), conversation.getContact(), false));
        binding!!.numberSelector.setSelection(getIndex(binding!!.numberSelector, number))
    }

    override fun hideNumberSpinner() {
        binding!!.numberSelector.visibility = View.GONE
    }

    override fun clearMsgEdit() {
        clearReply()
        binding!!.msgInputTxt.setText("")
    }

    override fun goToHome() {
        if (activity is ConversationActivity) {
            requireActivity().finish()
        } else {
            requireActivity().onBackPressedDispatcher.onBackPressed()
        }
    }

    override fun goToAddContact(contact: Contact) {
        startActivityForResult(ActionHelper.getAddNumberIntentForContact(contact), REQ_ADD_CONTACT)
    }


    override fun goToContactActivity(accountId: String, uri: net.jami.model.Uri) {
        val logo = binding!!.contactImage
        val intent = Intent(Intent.ACTION_VIEW, ConversationPath.toUri(accountId, uri))
            .setClass(requireContext().applicationContext, ContactDetailsActivity::class.java)
        startActivity(intent,
            ActivityOptions.makeSceneTransitionAnimation(activity, logo, "conversationIcon").toBundle())
    }

    override fun goToCallActivity(conferenceId: String, withCamera: Boolean) {
        startActivity(Intent(Intent.ACTION_VIEW)
            .setClass(requireContext(), CallActivity::class.java)
            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            .putExtra(NotificationService.KEY_CALL_ID, conferenceId))
    }

    override fun goToCallActivityWithResult(accountId: String, conversationUri: net.jami.model.Uri, contactUri: net.jami.model.Uri, withCamera: Boolean) {
        startActivity(Intent(Intent.ACTION_CALL)
            .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            .setClass(requireContext(), CallActivity::class.java)
            .putExtras(ConversationPath.toBundle(accountId, conversationUri))
            .putExtra(Intent.EXTRA_PHONE_NUMBER, contactUri.uri)
            .putExtra(CallFragment.KEY_HAS_VIDEO, withCamera))
    }

    private fun setupActionbar(conversation: ConversationItemViewModel) {
        val title = binding!!.contactTitle
        val subtitle = binding!!.contactSubtitle
        val logo = binding!!.contactImage
        logo.setImageDrawable(mConversationAvatar)
        logo.visibility = View.VISIBLE
        logo.setOnClickListener { openContact() }
        title.text = conversation.title
        title.textSize = 15f
       title.setTypeface(null, Typeface.NORMAL)
       if (conversation.uriTitle != conversation.title) {
            subtitle.text = conversation.uriTitle
            subtitle.visibility = View.VISIBLE
        } else {
            subtitle.text = ""
            subtitle.visibility = View.GONE
       }
    }

    fun blockContactRequest() {
        presenter.onBlockIncomingContactRequest()
    }

    fun refuseContactRequest() {
        presenter.onRefuseIncomingContactRequest()
    }

    fun acceptContactRequest() {
        presenter.onAcceptIncomingContactRequest()
    }

    fun addContact() {
        presenter.onAddContact()
    }

    override fun onPrepareOptionsMenu(menu: Menu) {
        super.onPrepareOptionsMenu(menu)
        val visible = binding!!.cvMessageInput.visibility == View.VISIBLE && !presenter.isGroup()
        mAudioCallBtn?.isVisible = visible
        mVideoCallBtn?.isVisible = visible
    }

    override fun switchToUnknownView(name: String) {
        binding?.apply {
            cvMessageInput.visibility = View.GONE
            unknownContactPrompt.visibility = View.VISIBLE
            trustRequestPrompt.visibility = View.GONE
            tvTrustRequestMessage.text = getString(R.string.message_contact_not_trusted, name)
            trustRequestMessageLayout.visibility = View.VISIBLE
            currentBottomView = unknownContactPrompt
        }
        requireActivity().invalidateOptionsMenu()
        updateListPadding()
    }

    override fun switchToIncomingTrustRequestView(name: String) {
        binding?.apply {
            cvMessageInput.visibility = View.GONE
            unknownContactPrompt.visibility = View.GONE
            trustRequestPrompt.visibility = View.VISIBLE
            tvTrustRequestMessage.text = getString(R.string.message_contact_not_trusted_yet, name)
            trustRequestMessageLayout.visibility = View.VISIBLE
            currentBottomView = trustRequestPrompt
        }
        requireActivity().invalidateOptionsMenu()
        updateListPadding()
    }

    override fun switchToConversationView() {
        binding?.apply {
            cvMessageInput.visibility = View.VISIBLE
            unknownContactPrompt.visibility = View.GONE
            trustRequestPrompt.visibility = View.GONE
            trustRequestMessageLayout.visibility = View.GONE
            currentBottomView = cvMessageInput
        }
        requireActivity().invalidateOptionsMenu()
        updateListPadding()
    }

    override fun switchToSyncingView() {
        binding?.apply {
            cvMessageInput.visibility = View.GONE
            unknownContactPrompt.visibility = View.GONE
            trustRequestPrompt.visibility = View.GONE
            trustRequestMessageLayout.visibility = View.VISIBLE
            tvTrustRequestMessage.text = getText(R.string.conversation_syncing)
        }
        currentBottomView = null
        requireActivity().invalidateOptionsMenu()
        updateListPadding()
    }

    override fun switchToBannedView() {
        binding?.apply {
            cvMessageInput.visibility = View.GONE
            unknownContactPrompt.visibility = View.GONE
            trustRequestPrompt.visibility = View.GONE
            trustRequestMessageLayout.visibility = View.VISIBLE
            tvTrustRequestMessage.text = getText(R.string.conversation_blocked)
        }
    }

    override fun switchToEndedView() {
        binding?.apply {
            cvMessageInput.visibility = View.GONE
            unknownContactPrompt.visibility = View.GONE
            trustRequestPrompt.visibility = View.GONE
            trustRequestMessageLayout.visibility = View.VISIBLE
            tvTrustRequestMessage.text = getText(R.string.conversation_ended)
        }
        currentBottomView = null
        requireActivity().invalidateOptionsMenu()
        updateListPadding()
    }

    override fun positiveMediaButtonClicked() {
        presenter.clickOnGoingPane()
    }

    override fun negativeMediaButtonClicked() {
        presenter.clickOnGoingPane()
    }

    override fun toggleMediaButtonClicked() {
        presenter.clickOnGoingPane()
    }

    private fun setLoading(isLoading: Boolean) {
        val binding = binding ?: return
        if (isLoading) {
            binding.btnTakePicture.visibility = View.GONE
            binding.pbDataTransfer.visibility = View.VISIBLE
        } else {
            binding.btnTakePicture.visibility = View.VISIBLE
            binding.pbDataTransfer.visibility = View.GONE
        }
    }

    fun handleShareIntent(intent: Intent) {
        Log.w(TAG, "handleShareIntent $intent")
        val action = intent.action
        if (Intent.ACTION_SEND == action || Intent.ACTION_SEND_MULTIPLE == action) {
            val type = intent.type
            if (type == null) {
                Log.w(TAG, "Can't share with no type")
                return
            }
            if (type.startsWith("text/plain")) {
                binding!!.msgInputTxt.setText(intent.getStringExtra(Intent.EXTRA_TEXT))
            } else {
                val intentUri = intent.data
                if (intentUri != null)
                    startFileSend(AndroidFileUtils.getCacheFile(requireContext(), intentUri).flatMapCompletable { file -> sendFile(file) })
                intent.clipData?.let { clipData ->
                    for (i in 0 until clipData.itemCount) {
                        val uri = clipData.getItemAt(i).uri
                        if (uri != intentUri)
                            startFileSend(AndroidFileUtils.getCacheFile(requireContext(), uri).flatMapCompletable { file -> sendFile(file) })
                    }
                }
            }
        } else if (Intent.ACTION_VIEW == action) {
            val path = ConversationPath.fromIntent(intent)
            if (path != null && intent.getBooleanExtra(EXTRA_SHOW_MAP, false)) {
                shareLocation()
            }
        }
    }

    /**
     * Creates an intent using Android Storage Access Framework
     * This intent is then received by applications that can handle it like
     * Downloads or Google drive
     * @param file DataTransfer of the file that is going to be stored
     * @param fileAbsolutePath absolute path of the file we want to save
     */
    override fun startSaveFile(file: DataTransfer, fileAbsolutePath: String) {
        //Get the current file absolute path and store it
        mCurrentFileAbsolutePath = fileAbsolutePath
        try {
            //Use Android Storage File Access to download the file
            val downloadFileIntent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
                type = AndroidFileUtils.getMimeTypeFromExtension(file.extension)
                addCategory(Intent.CATEGORY_OPENABLE)
                putExtra(Intent.EXTRA_TITLE, file.displayName)
            }
            startActivityForResult(downloadFileIntent, REQUEST_CODE_SAVE_FILE)
        } catch (e: Exception) {
            Log.i(TAG, "No app detected for saving files.")
            val directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
            if (!directory.exists()) {
                directory.mkdirs()
            }
            writeToFile(Uri.fromFile(File(directory, file.displayName)))
        }
    }

    override fun startReplyTo(interaction: Interaction) {
        replyingTo = interaction
        binding?.apply {
            if (interaction is TextMessage) {
                replyMessage.text = interaction.body
                replyMessage.isVisible = true
            }
            replyGroup.isVisible = true
        }
    }

    override fun displayNetworkErrorPanel() {
        binding?.apply {
            errorMsgPane.visibility = View.VISIBLE
            errorMsgPane.setOnClickListener(null)
            errorMsgPane.setText(R.string.error_no_network)
        }
    }

    override fun displayAccountOfflineErrorPanel() {
        binding?.apply {
            errorMsgPane.visibility = View.VISIBLE
            errorMsgPane.setOnClickListener(null)
            errorMsgPane.setText(R.string.error_account_offline)
            for (idx in 0 until btnContainer.childCount) {
                btnContainer.getChildAt(idx).isEnabled = false
            }
        }
    }

    override fun setSettings(readIndicator: Boolean, linkPreviews: Boolean) {
        mAdapter?.apply {
            setReadIndicatorStatus(readIndicator)
            showLinkPreviews = linkPreviews
        }
    }

    override fun hideErrorPanel() {
        binding?.errorMsgPane?.visibility = View.GONE
    }

    override fun goToSearchMessage(messageId: String) {
        binding?.toolbar?.menu?.findItem(R.id.conv_search)?.collapseActionView()
        binding?.histList?.doOnNextLayout {
            presenter.scrollToMessage(messageId)
        }
    }

    companion object {
        private val TAG = ConversationFragment::class.simpleName
        const val REQ_ADD_CONTACT = 42
        const val KEY_PREFERENCE_PENDING_MESSAGE = "pendingMessage"
        const val KEY_PREFERENCE_CONVERSATION_COLOR = "color"
        @Deprecated("Use daemon feature")
        const val KEY_PREFERENCE_CONVERSATION_LAST_READ = "lastRead"
        const val KEY_PREFERENCE_CONVERSATION_SYMBOL = "symbol"
        const val EXTRA_SHOW_MAP = "showMap"
        private const val REQUEST_CODE_FILE_PICKER = 1000
        private const val REQUEST_PERMISSION_CAMERA = 1001
        private const val REQUEST_CODE_TAKE_PICTURE = 1002
        private const val REQUEST_CODE_SAVE_FILE = 1003
        private const val REQUEST_CODE_CAPTURE_AUDIO = 1004
        private const val REQUEST_CODE_CAPTURE_VIDEO = 1005
        const val REQUEST_CODE_EDIT_MESSAGE = 1006
        private fun getIndex(spinner: Spinner, myString: net.jami.model.Uri): Int {
            var i = 0
            val n = spinner.count
            while (i < n) {
                if ((spinner.getItemAtPosition(i) as Phone).number == myString) {
                    return i
                }
                i++
            }
            return 0
        }

        private val SUPPORTED_MIME_TYPES = arrayOf("image/png", "image/jpg", "image/gif", "image/webp")
    }
}