ChatController.kt 136 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Mario Danic
  5. * @author Marcel Hibbe
  6. * @author Andy Scherzinger
  7. * @author Tim Krüger
  8. * Copyright (C) 2021 Tim Krüger <t@timkrueger.me>
  9. * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  10. * Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
  11. * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU General Public License as published by
  15. * the Free Software Foundation, either version 3 of the License, or
  16. * at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. */
  26. package com.nextcloud.talk.controllers
  27. import android.Manifest
  28. import android.annotation.SuppressLint
  29. import android.app.Activity.RESULT_OK
  30. import android.content.ClipData
  31. import android.content.ClipboardManager
  32. import android.content.Context
  33. import android.content.Intent
  34. import android.content.pm.PackageManager
  35. import android.content.res.AssetFileDescriptor
  36. import android.content.res.Resources
  37. import android.database.Cursor
  38. import android.graphics.Bitmap
  39. import android.graphics.drawable.ColorDrawable
  40. import android.media.MediaPlayer
  41. import android.media.MediaRecorder
  42. import android.net.Uri
  43. import android.os.Build
  44. import android.os.Build.VERSION_CODES.O
  45. import android.os.Bundle
  46. import android.os.Handler
  47. import android.os.Parcelable
  48. import android.os.SystemClock
  49. import android.os.VibrationEffect
  50. import android.os.Vibrator
  51. import android.provider.ContactsContract
  52. import android.provider.MediaStore
  53. import android.text.Editable
  54. import android.text.InputFilter
  55. import android.text.TextUtils
  56. import android.text.TextWatcher
  57. import android.util.Log
  58. import android.util.TypedValue
  59. import android.view.Gravity
  60. import android.view.Menu
  61. import android.view.MenuInflater
  62. import android.view.MenuItem
  63. import android.view.MotionEvent
  64. import android.view.View
  65. import android.view.animation.AlphaAnimation
  66. import android.view.animation.Animation
  67. import android.view.animation.LinearInterpolator
  68. import android.widget.AbsListView
  69. import android.widget.ImageButton
  70. import android.widget.ImageView
  71. import android.widget.PopupMenu
  72. import android.widget.RelativeLayout
  73. import android.widget.Toast
  74. import androidx.appcompat.app.AlertDialog
  75. import androidx.appcompat.view.ContextThemeWrapper
  76. import androidx.core.content.ContextCompat
  77. import androidx.core.content.FileProvider
  78. import androidx.core.content.PermissionChecker
  79. import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
  80. import androidx.core.widget.doAfterTextChanged
  81. import androidx.emoji.text.EmojiCompat
  82. import androidx.emoji.widget.EmojiTextView
  83. import androidx.recyclerview.widget.ItemTouchHelper
  84. import androidx.recyclerview.widget.LinearLayoutManager
  85. import androidx.recyclerview.widget.RecyclerView
  86. import androidx.work.Data
  87. import androidx.work.OneTimeWorkRequest
  88. import androidx.work.WorkInfo
  89. import androidx.work.WorkManager
  90. import autodagger.AutoInjector
  91. import coil.load
  92. import com.bluelinelabs.conductor.RouterTransaction
  93. import com.bluelinelabs.conductor.changehandler.HorizontalChangeHandler
  94. import com.facebook.common.executors.UiThreadImmediateExecutorService
  95. import com.facebook.common.references.CloseableReference
  96. import com.facebook.datasource.DataSource
  97. import com.facebook.drawee.backends.pipeline.Fresco
  98. import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
  99. import com.facebook.imagepipeline.image.CloseableImage
  100. import com.google.android.flexbox.FlexboxLayout
  101. import com.google.android.material.dialog.MaterialAlertDialogBuilder
  102. import com.nextcloud.talk.BuildConfig
  103. import com.nextcloud.talk.R
  104. import com.nextcloud.talk.activities.CallActivity
  105. import com.nextcloud.talk.activities.MainActivity
  106. import com.nextcloud.talk.activities.TakePhotoActivity
  107. import com.nextcloud.talk.adapters.messages.CommonMessageInterface
  108. import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
  109. import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
  110. import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
  111. import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
  112. import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
  113. import com.nextcloud.talk.adapters.messages.MagicIncomingTextMessageViewHolder
  114. import com.nextcloud.talk.adapters.messages.MagicOutcomingTextMessageViewHolder
  115. import com.nextcloud.talk.adapters.messages.MagicSystemMessageViewHolder
  116. import com.nextcloud.talk.adapters.messages.MagicUnreadNoticeMessageViewHolder
  117. import com.nextcloud.talk.adapters.messages.MessagePayload
  118. import com.nextcloud.talk.adapters.messages.OutcomingLinkPreviewMessageViewHolder
  119. import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
  120. import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
  121. import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
  122. import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
  123. import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
  124. import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
  125. import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
  126. import com.nextcloud.talk.api.NcApi
  127. import com.nextcloud.talk.application.NextcloudTalkApplication
  128. import com.nextcloud.talk.callbacks.MentionAutocompleteCallback
  129. import com.nextcloud.talk.controllers.base.BaseController
  130. import com.nextcloud.talk.controllers.util.viewBinding
  131. import com.nextcloud.talk.data.user.model.User
  132. import com.nextcloud.talk.databinding.ControllerChatBinding
  133. import com.nextcloud.talk.events.UserMentionClickEvent
  134. import com.nextcloud.talk.events.WebSocketCommunicationEvent
  135. import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
  136. import com.nextcloud.talk.jobs.ShareOperationWorker
  137. import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
  138. import com.nextcloud.talk.messagesearch.MessageSearchActivity
  139. import com.nextcloud.talk.models.json.chat.ChatMessage
  140. import com.nextcloud.talk.models.json.chat.ChatOverall
  141. import com.nextcloud.talk.models.json.chat.ChatOverallSingleMessage
  142. import com.nextcloud.talk.models.json.chat.ReadStatus
  143. import com.nextcloud.talk.models.json.conversations.Conversation
  144. import com.nextcloud.talk.models.json.conversations.RoomOverall
  145. import com.nextcloud.talk.models.json.conversations.RoomsOverall
  146. import com.nextcloud.talk.models.json.generic.GenericOverall
  147. import com.nextcloud.talk.models.json.mention.Mention
  148. import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
  149. import com.nextcloud.talk.presenters.MentionAutocompletePresenter
  150. import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
  151. import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
  152. import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
  153. import com.nextcloud.talk.ui.dialog.AttachmentDialog
  154. import com.nextcloud.talk.ui.dialog.MessageActionsDialog
  155. import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
  156. import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
  157. import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
  158. import com.nextcloud.talk.utils.ApiUtils
  159. import com.nextcloud.talk.utils.ConductorRemapping
  160. import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
  161. import com.nextcloud.talk.utils.ContactUtils
  162. import com.nextcloud.talk.utils.DateUtils
  163. import com.nextcloud.talk.utils.DisplayUtils
  164. import com.nextcloud.talk.utils.FileUtils
  165. import com.nextcloud.talk.utils.ImageEmojiEditText
  166. import com.nextcloud.talk.utils.MagicCharPolicy
  167. import com.nextcloud.talk.utils.NotificationUtils
  168. import com.nextcloud.talk.utils.ParticipantPermissions
  169. import com.nextcloud.talk.utils.bundle.BundleKeys
  170. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
  171. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
  172. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
  173. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
  174. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
  175. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
  176. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
  177. import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
  178. import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
  179. import com.nextcloud.talk.utils.rx.DisposableSet
  180. import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
  181. import com.nextcloud.talk.utils.text.Spans
  182. import com.nextcloud.talk.webrtc.MagicWebSocketInstance
  183. import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
  184. import com.otaliastudios.autocomplete.Autocomplete
  185. import com.stfalcon.chatkit.commons.ImageLoader
  186. import com.stfalcon.chatkit.commons.models.IMessage
  187. import com.stfalcon.chatkit.messages.MessageHolders
  188. import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
  189. import com.stfalcon.chatkit.messages.MessagesListAdapter
  190. import com.stfalcon.chatkit.utils.DateFormatter
  191. import com.vanniktech.emoji.EmojiPopup
  192. import io.reactivex.Observer
  193. import io.reactivex.android.schedulers.AndroidSchedulers
  194. import io.reactivex.disposables.Disposable
  195. import io.reactivex.schedulers.Schedulers
  196. import kotlinx.android.synthetic.main.view_message_input.view.*
  197. import org.greenrobot.eventbus.EventBus
  198. import org.greenrobot.eventbus.Subscribe
  199. import org.greenrobot.eventbus.ThreadMode
  200. import org.parceler.Parcels
  201. import retrofit2.HttpException
  202. import retrofit2.Response
  203. import java.io.File
  204. import java.io.IOException
  205. import java.net.HttpURLConnection
  206. import java.text.SimpleDateFormat
  207. import java.util.Date
  208. import java.util.Locale
  209. import java.util.Objects
  210. import java.util.concurrent.ExecutionException
  211. import javax.inject.Inject
  212. import kotlin.math.roundToInt
  213. @AutoInjector(NextcloudTalkApplication::class)
  214. class ChatController(args: Bundle) :
  215. BaseController(
  216. R.layout.controller_chat,
  217. args
  218. ),
  219. MessagesListAdapter.OnLoadMoreListener,
  220. MessagesListAdapter.Formatter<Date>,
  221. MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
  222. ContentChecker<ChatMessage>,
  223. VoiceMessageInterface,
  224. CommonMessageInterface,
  225. PreviewMessageInterface {
  226. private val binding: ControllerChatBinding by viewBinding(ControllerChatBinding::bind)
  227. @Inject
  228. lateinit var ncApi: NcApi
  229. @Inject
  230. lateinit var eventBus: EventBus
  231. @Inject
  232. lateinit var permissionUtil: PlatformPermissionUtil
  233. val disposables = DisposableSet()
  234. var roomToken: String? = null
  235. val conversationUser: User?
  236. private val roomPassword: String
  237. var credentials: String? = null
  238. var currentConversation: Conversation? = null
  239. var inConversation = false
  240. private var historyRead = false
  241. private var globalLastKnownFutureMessageId = -1
  242. private var globalLastKnownPastMessageId = -1
  243. var adapter: TalkMessagesListAdapter<ChatMessage>? = null
  244. private var mentionAutocomplete: Autocomplete<*>? = null
  245. var layoutManager: LinearLayoutManager? = null
  246. var pullChatMessagesPending = false
  247. private var lookingIntoFuture = false
  248. var newMessagesCount = 0
  249. var startCallFromNotification: Boolean? = null
  250. val roomId: String
  251. val voiceOnly: Boolean
  252. var isFirstMessagesProcessing = true
  253. private var emojiPopup: EmojiPopup? = null
  254. var myFirstMessage: CharSequence? = null
  255. var checkingLobbyStatus: Boolean = false
  256. private var conversationInfoMenuItem: MenuItem? = null
  257. private var conversationVoiceCallMenuItem: MenuItem? = null
  258. private var conversationVideoMenuItem: MenuItem? = null
  259. private var conversationSharedItemsItem: MenuItem? = null
  260. var magicWebSocketInstance: MagicWebSocketInstance? = null
  261. var lobbyTimerHandler: Handler? = null
  262. var pastPreconditionFailed = false
  263. var futurePreconditionFailed = false
  264. private val filesToUpload: MutableList<String> = ArrayList()
  265. private var sharedText: String
  266. var isVoiceRecordingInProgress: Boolean = false
  267. var currentVoiceRecordFile: String = ""
  268. private var recorder: MediaRecorder? = null
  269. var mediaPlayer: MediaPlayer? = null
  270. lateinit var mediaPlayerHandler: Handler
  271. private var currentlyPlayedVoiceMessage: ChatMessage? = null
  272. private lateinit var participantPermissions: ParticipantPermissions
  273. private var videoURI: Uri? = null
  274. init {
  275. Log.d(TAG, "init ChatController: " + System.identityHashCode(this).toString())
  276. setHasOptionsMenu(true)
  277. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  278. this.conversationUser = args.getParcelable(KEY_USER_ENTITY)
  279. this.roomId = args.getString(KEY_ROOM_ID, "")
  280. this.roomToken = args.getString(KEY_ROOM_TOKEN, "")
  281. this.sharedText = args.getString(BundleKeys.KEY_SHARED_TEXT, "")
  282. Log.d(TAG, " roomToken = $roomToken")
  283. if (roomToken.isNullOrEmpty()) {
  284. Log.d(TAG, " roomToken was null or empty!")
  285. }
  286. if (args.containsKey(KEY_ACTIVE_CONVERSATION)) {
  287. this.currentConversation = Parcels.unwrap<Conversation>(args.getParcelable(KEY_ACTIVE_CONVERSATION))
  288. this.participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!)
  289. }
  290. this.roomPassword = args.getString(BundleKeys.KEY_CONVERSATION_PASSWORD, "")
  291. credentials = if (conversationUser?.userId == "?") {
  292. null
  293. } else {
  294. ApiUtils.getCredentials(conversationUser!!.username, conversationUser.token)
  295. }
  296. if (args.containsKey(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)) {
  297. this.startCallFromNotification = args.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL)
  298. }
  299. this.voiceOnly = args.getBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, false)
  300. }
  301. private fun getRoomInfo() {
  302. val shouldRepeat = CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "webinary-lobby")
  303. if (shouldRepeat) {
  304. checkingLobbyStatus = true
  305. }
  306. if (conversationUser != null) {
  307. val apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  308. val startNanoTime = System.nanoTime()
  309. Log.d(TAG, "getRoomInfo - getRoom - calling: $startNanoTime")
  310. ncApi.getRoom(credentials, ApiUtils.getUrlForRoom(apiVersion, conversationUser.baseUrl, roomToken))
  311. ?.subscribeOn(Schedulers.io())
  312. ?.observeOn(AndroidSchedulers.mainThread())
  313. ?.subscribe(object : Observer<RoomOverall> {
  314. override fun onSubscribe(d: Disposable) {
  315. disposables.add(d)
  316. }
  317. @Suppress("Detekt.TooGenericExceptionCaught")
  318. override fun onNext(roomOverall: RoomOverall) {
  319. Log.d(TAG, "getRoomInfo - getRoom - got response: $startNanoTime")
  320. currentConversation = roomOverall.ocs!!.data
  321. Log.d(
  322. TAG,
  323. "getRoomInfo. token: " + currentConversation?.token +
  324. " sessionId: " + currentConversation?.sessionId
  325. )
  326. loadAvatarForStatusBar()
  327. setTitle()
  328. participantPermissions = ParticipantPermissions(conversationUser, currentConversation!!)
  329. try {
  330. setupSwipeToReply()
  331. setupMentionAutocomplete()
  332. checkShowCallButtons()
  333. checkShowMessageInputView()
  334. checkLobbyState()
  335. if (!inConversation) {
  336. joinRoomWithPassword()
  337. }
  338. } catch (npe: NullPointerException) {
  339. // view binding can be null
  340. // since this is called asynchronously and UI might have been destroyed in the meantime
  341. Log.i(TAG, "UI destroyed - view binding already gone")
  342. }
  343. }
  344. override fun onError(e: Throwable) {
  345. Log.e(TAG, "getRoomInfo - getRoom - ERROR", e)
  346. }
  347. override fun onComplete() {
  348. Log.d(TAG, "getRoomInfo - getRoom - onComplete: $startNanoTime")
  349. if (shouldRepeat) {
  350. if (lobbyTimerHandler == null) {
  351. lobbyTimerHandler = Handler()
  352. }
  353. lobbyTimerHandler?.postDelayed({ getRoomInfo() }, LOBBY_TIMER_DELAY)
  354. }
  355. }
  356. })
  357. }
  358. }
  359. private fun setupSwipeToReply() {
  360. if (this::participantPermissions.isInitialized &&
  361. participantPermissions.hasChatPermission() &&
  362. !isReadOnlyConversation()
  363. ) {
  364. val messageSwipeController = MessageSwipeCallback(
  365. activity!!,
  366. object : MessageSwipeActions {
  367. override fun showReplyUI(position: Int) {
  368. val chatMessage = adapter?.items?.get(position)?.item as ChatMessage?
  369. replyToMessage(chatMessage)
  370. }
  371. }
  372. )
  373. withNullableControllerViewBinding {
  374. val itemTouchHelper = ItemTouchHelper(messageSwipeController)
  375. itemTouchHelper.attachToRecyclerView(binding.messagesListView)
  376. }
  377. }
  378. }
  379. private fun handleFromNotification() {
  380. var apiVersion = 1
  381. // FIXME Can this be called for guests?
  382. if (conversationUser != null) {
  383. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  384. }
  385. Log.d(TAG, "handleFromNotification - getRooms - calling")
  386. ncApi.getRooms(credentials, ApiUtils.getUrlForRooms(apiVersion, conversationUser?.baseUrl))
  387. ?.subscribeOn(Schedulers.io())?.observeOn(AndroidSchedulers.mainThread())
  388. ?.subscribe(object : Observer<RoomsOverall> {
  389. override fun onSubscribe(d: Disposable) {
  390. disposables.add(d)
  391. }
  392. override fun onNext(roomsOverall: RoomsOverall) {
  393. Log.d(TAG, "handleFromNotification - getRooms - got response")
  394. for (conversation in roomsOverall.ocs!!.data!!) {
  395. if (roomId == conversation.roomId) {
  396. roomToken = conversation.token
  397. currentConversation = conversation
  398. participantPermissions = ParticipantPermissions(conversationUser!!, currentConversation!!)
  399. setTitle()
  400. getRoomInfo()
  401. break
  402. }
  403. }
  404. }
  405. override fun onError(e: Throwable) {
  406. Log.e(TAG, "handleFromNotification - getRooms - ERROR: ", e)
  407. }
  408. override fun onComplete() {
  409. // unused atm
  410. }
  411. })
  412. }
  413. private fun loadAvatarForStatusBar() {
  414. if (isOneToOneConversation() && activity != null) {
  415. val imageRequest = DisplayUtils.getImageRequestForUrl(
  416. ApiUtils.getUrlForAvatar(
  417. conversationUser?.baseUrl,
  418. currentConversation?.name,
  419. true
  420. ),
  421. conversationUser!!
  422. )
  423. val imagePipeline = Fresco.getImagePipeline()
  424. val dataSource = imagePipeline.fetchDecodedImage(imageRequest, null)
  425. dataSource.subscribe(
  426. object : BaseBitmapDataSubscriber() {
  427. override fun onNewResultImpl(bitmap: Bitmap?) {
  428. if (actionBar != null && bitmap != null && resources != null) {
  429. val avatarSize = (actionBar?.height!! / TOOLBAR_AVATAR_RATIO).roundToInt()
  430. if (avatarSize > 0) {
  431. val bitmapResized = Bitmap.createScaledBitmap(bitmap, avatarSize, avatarSize, false)
  432. val roundedBitmapDrawable =
  433. RoundedBitmapDrawableFactory.create(resources!!, bitmapResized)
  434. roundedBitmapDrawable.isCircular = true
  435. roundedBitmapDrawable.setAntiAlias(true)
  436. actionBar?.setIcon(roundedBitmapDrawable)
  437. } else {
  438. Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0")
  439. }
  440. }
  441. }
  442. override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage>>) {
  443. // unused atm
  444. }
  445. },
  446. UiThreadImmediateExecutorService.getInstance()
  447. )
  448. }
  449. }
  450. fun isOneToOneConversation() = currentConversation != null && currentConversation?.type != null &&
  451. currentConversation?.type == Conversation.ConversationType
  452. .ROOM_TYPE_ONE_TO_ONE_CALL
  453. @Suppress("Detekt.TooGenericExceptionCaught")
  454. override fun onViewBound(view: View) {
  455. Log.d(TAG, "onViewBound: " + System.identityHashCode(this).toString())
  456. actionBar?.show()
  457. var adapterWasNull = false
  458. if (adapter == null) {
  459. binding.progressBar.visibility = View.VISIBLE
  460. adapterWasNull = true
  461. val messageHolders = MessageHolders()
  462. val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!, router)
  463. val payload =
  464. MessagePayload(roomToken!!, currentConversation?.isParticipantOwnerOrModerator, profileBottomSheet)
  465. messageHolders.setIncomingTextConfig(
  466. MagicIncomingTextMessageViewHolder::class.java,
  467. R.layout.item_custom_incoming_text_message,
  468. payload
  469. )
  470. messageHolders.setOutcomingTextConfig(
  471. MagicOutcomingTextMessageViewHolder::class.java,
  472. R.layout.item_custom_outcoming_text_message
  473. )
  474. messageHolders.setIncomingImageConfig(
  475. IncomingPreviewMessageViewHolder::class.java,
  476. R.layout.item_custom_incoming_preview_message,
  477. payload
  478. )
  479. messageHolders.setOutcomingImageConfig(
  480. OutcomingPreviewMessageViewHolder::class.java,
  481. R.layout.item_custom_outcoming_preview_message
  482. )
  483. messageHolders.registerContentType(
  484. CONTENT_TYPE_SYSTEM_MESSAGE,
  485. MagicSystemMessageViewHolder::class.java,
  486. R.layout.item_system_message,
  487. MagicSystemMessageViewHolder::class.java,
  488. R.layout.item_system_message,
  489. this
  490. )
  491. messageHolders.registerContentType(
  492. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE,
  493. MagicUnreadNoticeMessageViewHolder::class.java,
  494. R.layout.item_date_header,
  495. MagicUnreadNoticeMessageViewHolder::class.java,
  496. R.layout.item_date_header,
  497. this
  498. )
  499. messageHolders.registerContentType(
  500. CONTENT_TYPE_LOCATION,
  501. IncomingLocationMessageViewHolder::class.java,
  502. payload,
  503. R.layout.item_custom_incoming_location_message,
  504. OutcomingLocationMessageViewHolder::class.java,
  505. null,
  506. R.layout.item_custom_outcoming_location_message,
  507. this
  508. )
  509. messageHolders.registerContentType(
  510. CONTENT_TYPE_VOICE_MESSAGE,
  511. IncomingVoiceMessageViewHolder::class.java,
  512. payload,
  513. R.layout.item_custom_incoming_voice_message,
  514. OutcomingVoiceMessageViewHolder::class.java,
  515. null,
  516. R.layout.item_custom_outcoming_voice_message,
  517. this
  518. )
  519. messageHolders.registerContentType(
  520. CONTENT_TYPE_POLL,
  521. IncomingPollMessageViewHolder::class.java,
  522. payload,
  523. R.layout.item_custom_incoming_poll_message,
  524. OutcomingPollMessageViewHolder::class.java,
  525. payload,
  526. R.layout.item_custom_outcoming_poll_message,
  527. this
  528. )
  529. messageHolders.registerContentType(
  530. CONTENT_TYPE_LINK_PREVIEW,
  531. IncomingLinkPreviewMessageViewHolder::class.java,
  532. payload,
  533. R.layout.item_custom_incoming_link_preview_message,
  534. OutcomingLinkPreviewMessageViewHolder::class.java,
  535. payload,
  536. R.layout.item_custom_outcoming_link_preview_message,
  537. this
  538. )
  539. val senderId = if (!conversationUser.userId.equals("?")) {
  540. "users/" + conversationUser.userId
  541. } else {
  542. currentConversation?.actorType + "/" + currentConversation?.actorId
  543. }
  544. Log.d(TAG, "Initialize TalkMessagesListAdapter with senderId: $senderId")
  545. adapter = TalkMessagesListAdapter(
  546. senderId,
  547. messageHolders,
  548. ImageLoader { imageView, url, payload ->
  549. val draweeController = Fresco.newDraweeControllerBuilder()
  550. .setImageRequest(DisplayUtils.getImageRequestForUrl(url, conversationUser))
  551. .setControllerListener(DisplayUtils.getImageControllerListener(imageView))
  552. .setOldController(imageView.controller)
  553. .setAutoPlayAnimations(true)
  554. .build()
  555. imageView.controller = draweeController
  556. },
  557. this
  558. )
  559. } else {
  560. binding.messagesListView.visibility = View.VISIBLE
  561. }
  562. binding.messagesListView.setAdapter(adapter)
  563. adapter?.setLoadMoreListener(this)
  564. adapter?.setDateHeadersFormatter { format(it) }
  565. adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
  566. adapter?.registerViewClickListener(
  567. R.id.playPauseBtn
  568. ) { view, message ->
  569. val filename = message.selectedIndividualHashMap!!["name"]
  570. val file = File(context.cacheDir, filename!!)
  571. if (file.exists()) {
  572. if (message.isPlayingVoiceMessage) {
  573. pausePlayback(message)
  574. } else {
  575. startPlayback(message)
  576. }
  577. } else {
  578. downloadFileToCache(message)
  579. }
  580. }
  581. setupSwipeToReply()
  582. layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
  583. binding.popupBubbleView.setRecyclerView(binding.messagesListView)
  584. binding.popupBubbleView.setPopupBubbleListener { context ->
  585. if (newMessagesCount != 0) {
  586. val scrollPosition = if (newMessagesCount - 1 < 0) {
  587. 0
  588. } else {
  589. newMessagesCount - 1
  590. }
  591. Handler().postDelayed(
  592. {
  593. binding.messagesListView.smoothScrollToPosition(scrollPosition)
  594. },
  595. NEW_MESSAGES_POPUP_BUBBLE_DELAY
  596. )
  597. }
  598. }
  599. viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.popupBubbleView)
  600. binding.messageInputView.setPadding(0, 0, 0, 0)
  601. binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
  602. override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
  603. super.onScrollStateChanged(recyclerView, newState)
  604. if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
  605. if (newMessagesCount != 0 && layoutManager != null) {
  606. if (layoutManager!!.findFirstCompletelyVisibleItemPosition() < newMessagesCount) {
  607. newMessagesCount = 0
  608. if (binding.popupBubbleView.isShown) {
  609. binding.popupBubbleView.hide()
  610. }
  611. }
  612. }
  613. }
  614. }
  615. })
  616. val filters = arrayOfNulls<InputFilter>(1)
  617. val lengthFilter = CapabilitiesUtilNew.getMessageMaxLength(conversationUser)
  618. filters[0] = InputFilter.LengthFilter(lengthFilter)
  619. binding.messageInputView.inputEditText?.filters = filters
  620. binding.messageInputView.inputEditText?.addTextChangedListener(object : TextWatcher {
  621. override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
  622. // unused atm
  623. }
  624. @Suppress("Detekt.TooGenericExceptionCaught")
  625. override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
  626. try {
  627. if (s.length >= lengthFilter) {
  628. binding.messageInputView.inputEditText?.error = String.format(
  629. Objects.requireNonNull<Resources>(resources).getString(R.string.nc_limit_hit),
  630. lengthFilter.toString()
  631. )
  632. } else {
  633. binding.messageInputView.inputEditText?.error = null
  634. }
  635. val editable = binding.messageInputView.inputEditText?.editableText
  636. if (editable != null && binding.messageInputView.inputEditText != null) {
  637. val mentionSpans = editable.getSpans(
  638. 0,
  639. binding.messageInputView.inputEditText!!.length(),
  640. Spans.MentionChipSpan::class.java
  641. )
  642. var mentionSpan: Spans.MentionChipSpan
  643. for (i in mentionSpans.indices) {
  644. mentionSpan = mentionSpans[i]
  645. if (start >= editable.getSpanStart(mentionSpan) &&
  646. start < editable.getSpanEnd(mentionSpan)
  647. ) {
  648. if (editable.subSequence(
  649. editable.getSpanStart(mentionSpan),
  650. editable.getSpanEnd(mentionSpan)
  651. ).toString().trim { it <= ' ' } != mentionSpan.label
  652. ) {
  653. editable.removeSpan(mentionSpan)
  654. }
  655. }
  656. }
  657. }
  658. } catch (npe: NullPointerException) {
  659. // view binding can be null
  660. // since this is called asynchronously and UI might have been destroyed in the meantime
  661. Log.i(TAG, "UI destroyed - view binding already gone")
  662. }
  663. }
  664. override fun afterTextChanged(s: Editable) {
  665. // unused atm
  666. }
  667. })
  668. // Image keyboard support
  669. // See: https://developer.android.com/guide/topics/text/image-keyboard
  670. (binding.messageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
  671. uploadFile(it.toString(), false)
  672. }
  673. showMicrophoneButton(true)
  674. binding.messageInputView.messageInput.doAfterTextChanged {
  675. try {
  676. if (binding.messageInputView.messageInput.text.isEmpty()) {
  677. showMicrophoneButton(true)
  678. } else {
  679. showMicrophoneButton(false)
  680. }
  681. } catch (npe: NullPointerException) {
  682. // view binding can be null
  683. // since this is called asynchronously and UI might have been destroyed in the meantime
  684. Log.i(TAG, "UI destroyed - view binding already gone")
  685. }
  686. }
  687. var sliderInitX = 0F
  688. var downX = 0f
  689. var deltaX = 0f
  690. var voiceRecordStartTime = 0L
  691. var voiceRecordEndTime = 0L
  692. binding.messageInputView.recordAudioButton.setOnTouchListener(object : View.OnTouchListener {
  693. override fun onTouch(v: View?, event: MotionEvent?): Boolean {
  694. view.performClick()
  695. when (event?.action) {
  696. MotionEvent.ACTION_DOWN -> {
  697. if (!isRecordAudioPermissionGranted()) {
  698. requestRecordAudioPermissions()
  699. return true
  700. }
  701. if (!UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
  702. UploadAndShareFilesWorker.requestStoragePermission(this@ChatController)
  703. return true
  704. }
  705. voiceRecordStartTime = System.currentTimeMillis()
  706. setVoiceRecordFileName()
  707. startAudioRecording(currentVoiceRecordFile)
  708. downX = event.x
  709. showRecordAudioUi(true)
  710. }
  711. MotionEvent.ACTION_CANCEL -> {
  712. Log.d(TAG, "ACTION_CANCEL. same as for UP")
  713. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  714. return true
  715. }
  716. stopAndDiscardAudioRecording()
  717. showRecordAudioUi(false)
  718. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  719. }
  720. MotionEvent.ACTION_UP -> {
  721. Log.d(TAG, "ACTION_UP. stop recording??")
  722. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  723. return true
  724. }
  725. showRecordAudioUi(false)
  726. voiceRecordEndTime = System.currentTimeMillis()
  727. val voiceRecordDuration = voiceRecordEndTime - voiceRecordStartTime
  728. if (voiceRecordDuration < MINIMUM_VOICE_RECORD_DURATION) {
  729. Log.d(TAG, "voiceRecordDuration: $voiceRecordDuration")
  730. Toast.makeText(
  731. context,
  732. context.getString(R.string.nc_voice_message_hold_to_record_info),
  733. Toast.LENGTH_SHORT
  734. ).show()
  735. stopAndDiscardAudioRecording()
  736. return true
  737. } else {
  738. voiceRecordStartTime = 0L
  739. voiceRecordEndTime = 0L
  740. stopAndSendAudioRecording()
  741. }
  742. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  743. }
  744. MotionEvent.ACTION_MOVE -> {
  745. Log.d(TAG, "ACTION_MOVE.")
  746. if (!isVoiceRecordingInProgress || !isRecordAudioPermissionGranted()) {
  747. return true
  748. }
  749. showRecordAudioUi(true)
  750. if (sliderInitX == 0.0F) {
  751. sliderInitX = binding.messageInputView.slideToCancelDescription.x
  752. }
  753. val movedX: Float = event.x
  754. deltaX = movedX - downX
  755. // only allow slide to left
  756. if (binding.messageInputView.slideToCancelDescription.x > sliderInitX) {
  757. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  758. }
  759. if (binding.messageInputView.slideToCancelDescription.x < VOICE_RECORD_CANCEL_SLIDER_X) {
  760. Log.d(TAG, "stopping recording because slider was moved to left")
  761. stopAndDiscardAudioRecording()
  762. showRecordAudioUi(false)
  763. binding.messageInputView.slideToCancelDescription.x = sliderInitX
  764. return true
  765. } else {
  766. binding.messageInputView.slideToCancelDescription.x = binding.messageInputView
  767. .slideToCancelDescription.x + deltaX
  768. downX = movedX
  769. }
  770. }
  771. }
  772. return v?.onTouchEvent(event) ?: true
  773. }
  774. })
  775. binding.messageInputView.inputEditText?.setText(sharedText)
  776. binding.messageInputView.setAttachmentsListener {
  777. activity?.let { AttachmentDialog(it, this).show() }
  778. }
  779. binding.messageInputView.button.setOnClickListener { submitMessage(false) }
  780. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "silent-send")) {
  781. binding.messageInputView.button.setOnLongClickListener {
  782. showSendButtonMenu()
  783. true
  784. }
  785. }
  786. binding.messageInputView.button.contentDescription = resources?.getString(
  787. R.string
  788. .nc_description_send_message_button
  789. )
  790. viewThemeUtils.platform.colorImageView(binding.messageInputView.button)
  791. if (currentConversation != null && currentConversation?.roomId != null) {
  792. loadAvatarForStatusBar()
  793. setTitle()
  794. }
  795. if (adapterWasNull) {
  796. // we're starting
  797. if (TextUtils.isEmpty(roomToken)) {
  798. handleFromNotification()
  799. } else {
  800. getRoomInfo()
  801. }
  802. }
  803. super.onViewBound(view)
  804. }
  805. private fun showSendButtonMenu() {
  806. val popupMenu = PopupMenu(
  807. ContextThemeWrapper(view?.context, R.style.ChatSendButtonMenu),
  808. binding.messageInputView.button,
  809. Gravity.END
  810. )
  811. popupMenu.inflate(R.menu.chat_send_menu)
  812. popupMenu.setOnMenuItemClickListener { item: MenuItem ->
  813. when (item.itemId) {
  814. R.id.send_without_notification -> submitMessage(true)
  815. }
  816. true
  817. }
  818. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  819. popupMenu.setForceShowIcon(true)
  820. }
  821. popupMenu.show()
  822. }
  823. private fun showCallButtonMenu(isVoiceOnlyCall: Boolean) {
  824. val anchor: View? = if (isVoiceOnlyCall) {
  825. activity?.findViewById(R.id.conversation_voice_call)
  826. } else {
  827. activity?.findViewById(R.id.conversation_video_call)
  828. }
  829. if (anchor != null) {
  830. val popupMenu = PopupMenu(
  831. ContextThemeWrapper(view?.context, R.style.CallButtonMenu),
  832. anchor,
  833. Gravity.END
  834. )
  835. popupMenu.inflate(R.menu.chat_call_menu)
  836. popupMenu.setOnMenuItemClickListener { item: MenuItem ->
  837. when (item.itemId) {
  838. R.id.call_without_notification -> startACall(isVoiceOnlyCall, true)
  839. }
  840. true
  841. }
  842. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  843. popupMenu.setForceShowIcon(true)
  844. }
  845. popupMenu.show()
  846. }
  847. }
  848. private fun startPlayback(message: ChatMessage) {
  849. if (!this.isAttached) {
  850. // don't begin to play voice message if screen is not visible anymore.
  851. // this situation might happen if file is downloading but user already left the chatview.
  852. // If user returns to chatview, the old chatview instance is not attached anymore
  853. // and he has to click the play button again (which is considered to be okay)
  854. return
  855. }
  856. initMediaPlayer(message)
  857. if (!mediaPlayer!!.isPlaying) {
  858. mediaPlayer!!.start()
  859. }
  860. mediaPlayerHandler = Handler()
  861. activity?.runOnUiThread(object : Runnable {
  862. override fun run() {
  863. if (mediaPlayer != null) {
  864. val currentPosition: Int = mediaPlayer!!.currentPosition / VOICE_MESSAGE_SEEKBAR_BASE
  865. message.voiceMessagePlayedSeconds = currentPosition
  866. adapter?.update(message)
  867. }
  868. mediaPlayerHandler.postDelayed(this, SECOND)
  869. }
  870. })
  871. message.isDownloadingVoiceMessage = false
  872. message.isPlayingVoiceMessage = true
  873. adapter?.update(message)
  874. }
  875. private fun pausePlayback(message: ChatMessage) {
  876. if (mediaPlayer!!.isPlaying) {
  877. mediaPlayer!!.pause()
  878. }
  879. message.isPlayingVoiceMessage = false
  880. adapter?.update(message)
  881. }
  882. private fun initMediaPlayer(message: ChatMessage) {
  883. if (message != currentlyPlayedVoiceMessage) {
  884. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  885. }
  886. if (mediaPlayer == null) {
  887. val fileName = message.selectedIndividualHashMap!!["name"]
  888. val absolutePath = context.cacheDir.absolutePath + "/" + fileName
  889. mediaPlayer = MediaPlayer().apply {
  890. setDataSource(absolutePath)
  891. prepare()
  892. }
  893. currentlyPlayedVoiceMessage = message
  894. message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE
  895. mediaPlayer!!.setOnCompletionListener {
  896. stopMediaPlayer(message)
  897. }
  898. } else {
  899. Log.e(TAG, "mediaPlayer was not null. This should not happen!")
  900. }
  901. }
  902. private fun stopMediaPlayer(message: ChatMessage) {
  903. message.isPlayingVoiceMessage = false
  904. message.resetVoiceMessage = true
  905. adapter?.update(message)
  906. currentlyPlayedVoiceMessage = null
  907. mediaPlayerHandler.removeCallbacksAndMessages(null)
  908. mediaPlayer?.stop()
  909. mediaPlayer?.release()
  910. mediaPlayer = null
  911. }
  912. override fun updateMediaPlayerProgressBySlider(messageWithSlidedProgress: ChatMessage, progress: Int) {
  913. if (mediaPlayer != null) {
  914. if (messageWithSlidedProgress == currentlyPlayedVoiceMessage) {
  915. mediaPlayer!!.seekTo(progress * VOICE_MESSAGE_SEEKBAR_BASE)
  916. }
  917. }
  918. }
  919. @SuppressLint("LongLogTag")
  920. private fun downloadFileToCache(message: ChatMessage) {
  921. message.isDownloadingVoiceMessage = true
  922. adapter?.update(message)
  923. val baseUrl = message.activeUser!!.baseUrl
  924. val userId = message.activeUser!!.userId
  925. val attachmentFolder = CapabilitiesUtilNew.getAttachmentFolder(message.activeUser!!)
  926. val fileName = message.selectedIndividualHashMap!!["name"]
  927. var size = message.selectedIndividualHashMap!!["size"]
  928. if (size == null) {
  929. size = "-1"
  930. }
  931. val fileSize = size.toLong()
  932. val fileId = message.selectedIndividualHashMap!!["id"]
  933. val path = message.selectedIndividualHashMap!!["path"]
  934. // check if download worker is already running
  935. val workers = WorkManager.getInstance(
  936. context
  937. ).getWorkInfosByTag(fileId!!)
  938. try {
  939. for (workInfo in workers.get()) {
  940. if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
  941. Log.d(TAG, "Download worker for $fileId is already running or scheduled")
  942. return
  943. }
  944. }
  945. } catch (e: ExecutionException) {
  946. Log.e(TAG, "Error when checking if worker already exists", e)
  947. } catch (e: InterruptedException) {
  948. Log.e(TAG, "Error when checking if worker already exists", e)
  949. }
  950. val data: Data = Data.Builder()
  951. .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
  952. .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
  953. .putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
  954. .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
  955. .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
  956. .putLong(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
  957. .build()
  958. val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
  959. .setInputData(data)
  960. .addTag(fileId)
  961. .build()
  962. WorkManager.getInstance().enqueue(downloadWorker)
  963. WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
  964. .observeForever { workInfo: WorkInfo ->
  965. if (workInfo.state == WorkInfo.State.SUCCEEDED) {
  966. startPlayback(message)
  967. }
  968. }
  969. }
  970. @SuppressLint("SimpleDateFormat")
  971. private fun setVoiceRecordFileName() {
  972. val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
  973. val date: String = simpleDateFormat.format(Date())
  974. val fileNameWithoutSuffix = String.format(
  975. context.resources.getString(R.string.nc_voice_message_filename),
  976. date,
  977. currentConversation!!.displayName
  978. )
  979. val fileName = fileNameWithoutSuffix + VOICE_MESSAGE_FILE_SUFFIX
  980. currentVoiceRecordFile = "${context.cacheDir.absolutePath}/$fileName"
  981. }
  982. private fun showRecordAudioUi(show: Boolean) {
  983. if (show) {
  984. binding.messageInputView.microphoneEnabledInfo.visibility = View.VISIBLE
  985. binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.VISIBLE
  986. binding.messageInputView.audioRecordDuration.visibility = View.VISIBLE
  987. binding.messageInputView.slideToCancelDescription.visibility = View.VISIBLE
  988. binding.messageInputView.attachmentButton.visibility = View.GONE
  989. binding.messageInputView.smileyButton.visibility = View.GONE
  990. binding.messageInputView.messageInput.visibility = View.GONE
  991. binding.messageInputView.messageInput.hint = ""
  992. } else {
  993. binding.messageInputView.microphoneEnabledInfo.visibility = View.GONE
  994. binding.messageInputView.microphoneEnabledInfoBackground.visibility = View.GONE
  995. binding.messageInputView.audioRecordDuration.visibility = View.GONE
  996. binding.messageInputView.slideToCancelDescription.visibility = View.GONE
  997. binding.messageInputView.attachmentButton.visibility = View.VISIBLE
  998. binding.messageInputView.smileyButton.visibility = View.VISIBLE
  999. binding.messageInputView.messageInput.visibility = View.VISIBLE
  1000. binding.messageInputView.messageInput.hint =
  1001. context.resources?.getString(R.string.nc_hint_enter_a_message)
  1002. }
  1003. }
  1004. private fun isRecordAudioPermissionGranted(): Boolean {
  1005. return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  1006. return PermissionChecker.checkSelfPermission(
  1007. context,
  1008. Manifest.permission.RECORD_AUDIO
  1009. ) == PermissionChecker.PERMISSION_GRANTED
  1010. } else {
  1011. true
  1012. }
  1013. }
  1014. private fun startAudioRecording(file: String) {
  1015. binding.messageInputView.audioRecordDuration.base = SystemClock.elapsedRealtime()
  1016. binding.messageInputView.audioRecordDuration.start()
  1017. val animation: Animation = AlphaAnimation(1.0f, 0.0f)
  1018. animation.duration = ANIMATION_DURATION
  1019. animation.interpolator = LinearInterpolator()
  1020. animation.repeatCount = Animation.INFINITE
  1021. animation.repeatMode = Animation.REVERSE
  1022. binding.messageInputView.microphoneEnabledInfo.startAnimation(animation)
  1023. recorder = MediaRecorder().apply {
  1024. setAudioSource(MediaRecorder.AudioSource.MIC)
  1025. setOutputFile(file)
  1026. setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
  1027. setAudioEncoder(MediaRecorder.AudioEncoder.AAC)
  1028. try {
  1029. prepare()
  1030. } catch (e: IOException) {
  1031. Log.e(TAG, "prepare for audio recording failed")
  1032. }
  1033. try {
  1034. start()
  1035. isVoiceRecordingInProgress = true
  1036. } catch (e: IllegalStateException) {
  1037. Log.e(TAG, "start for audio recording failed")
  1038. }
  1039. vibrate()
  1040. }
  1041. }
  1042. private fun stopAndSendAudioRecording() {
  1043. stopAudioRecording()
  1044. val uri = Uri.fromFile(File(currentVoiceRecordFile))
  1045. uploadFile(uri.toString(), true)
  1046. }
  1047. private fun stopAndDiscardAudioRecording() {
  1048. stopAudioRecording()
  1049. val cachedFile = File(currentVoiceRecordFile)
  1050. cachedFile.delete()
  1051. }
  1052. @Suppress("Detekt.TooGenericExceptionCaught")
  1053. private fun stopAudioRecording() {
  1054. binding.messageInputView.audioRecordDuration.stop()
  1055. binding.messageInputView.microphoneEnabledInfo.clearAnimation()
  1056. if (isVoiceRecordingInProgress) {
  1057. recorder?.apply {
  1058. try {
  1059. stop()
  1060. release()
  1061. isVoiceRecordingInProgress = false
  1062. Log.d(TAG, "stopped recorder. isVoiceRecordingInProgress = false")
  1063. } catch (e: RuntimeException) {
  1064. Log.w(TAG, "error while stopping recorder!")
  1065. }
  1066. vibrate()
  1067. }
  1068. recorder = null
  1069. } else {
  1070. Log.e(TAG, "tried to stop audio recorder but it was not recording")
  1071. }
  1072. }
  1073. fun vibrate() {
  1074. val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
  1075. if (Build.VERSION.SDK_INT >= O) {
  1076. vibrator.vibrate(VibrationEffect.createOneShot(SHORT_VIBRATE, VibrationEffect.DEFAULT_AMPLITUDE))
  1077. } else {
  1078. vibrator.vibrate(SHORT_VIBRATE)
  1079. }
  1080. }
  1081. private fun requestRecordAudioPermissions() {
  1082. requestPermissions(
  1083. arrayOf(
  1084. Manifest.permission.RECORD_AUDIO
  1085. ),
  1086. REQUEST_RECORD_AUDIO_PERMISSION
  1087. )
  1088. }
  1089. private fun requestCameraPermissions() {
  1090. requestPermissions(
  1091. arrayOf(
  1092. Manifest.permission.CAMERA
  1093. ),
  1094. REQUEST_CAMERA_PERMISSION
  1095. )
  1096. }
  1097. private fun requestReadContacts() {
  1098. requestPermissions(
  1099. arrayOf(
  1100. Manifest.permission.READ_CONTACTS
  1101. ),
  1102. REQUEST_READ_CONTACT_PERMISSION
  1103. )
  1104. }
  1105. private fun checkShowCallButtons() {
  1106. if (isAlive()) {
  1107. if (isReadOnlyConversation() || shouldShowLobby()) {
  1108. disableCallButtons()
  1109. } else {
  1110. enableCallButtons()
  1111. }
  1112. }
  1113. }
  1114. private fun checkShowMessageInputView() {
  1115. if (isAlive()) {
  1116. if (isReadOnlyConversation() ||
  1117. shouldShowLobby() ||
  1118. !participantPermissions.hasChatPermission()
  1119. ) {
  1120. binding.messageInputView.visibility = View.GONE
  1121. } else {
  1122. binding.messageInputView.visibility = View.VISIBLE
  1123. }
  1124. }
  1125. }
  1126. private fun shouldShowLobby(): Boolean {
  1127. if (currentConversation != null) {
  1128. return currentConversation?.lobbyState == Conversation.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
  1129. currentConversation?.canModerate(conversationUser!!) == false &&
  1130. !participantPermissions.canIgnoreLobby()
  1131. }
  1132. return false
  1133. }
  1134. private fun disableCallButtons() {
  1135. if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
  1136. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1137. conversationVoiceCallMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1138. conversationVideoMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1139. conversationVoiceCallMenuItem?.isEnabled = false
  1140. conversationVideoMenuItem?.isEnabled = false
  1141. } else {
  1142. Log.e(TAG, "call buttons were null when trying to disable them")
  1143. }
  1144. }
  1145. }
  1146. private fun enableCallButtons() {
  1147. if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
  1148. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1149. conversationVoiceCallMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1150. conversationVideoMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1151. conversationVoiceCallMenuItem?.isEnabled = true
  1152. conversationVideoMenuItem?.isEnabled = true
  1153. } else {
  1154. Log.e(TAG, "call buttons were null when trying to enable them")
  1155. }
  1156. }
  1157. }
  1158. private fun isReadOnlyConversation(): Boolean {
  1159. return currentConversation?.conversationReadOnlyState != null &&
  1160. currentConversation?.conversationReadOnlyState ==
  1161. Conversation.ConversationReadOnlyState.CONVERSATION_READ_ONLY
  1162. }
  1163. private fun checkLobbyState() {
  1164. if (currentConversation != null &&
  1165. currentConversation?.isLobbyViewApplicable(conversationUser!!) == true &&
  1166. isAlive()
  1167. ) {
  1168. if (!checkingLobbyStatus) {
  1169. getRoomInfo()
  1170. }
  1171. if (shouldShowLobby()) {
  1172. binding.lobby.lobbyView.visibility = View.VISIBLE
  1173. binding.messagesListView.visibility = View.GONE
  1174. binding.messageInputView.visibility = View.GONE
  1175. binding.progressBar.visibility = View.GONE
  1176. val sb = StringBuilder()
  1177. sb.append(resources!!.getText(R.string.nc_lobby_waiting))
  1178. .append("\n\n")
  1179. if (currentConversation?.lobbyTimer != null && currentConversation?.lobbyTimer !=
  1180. 0L
  1181. ) {
  1182. val timestamp = currentConversation?.lobbyTimer ?: 0
  1183. val stringWithStartDate = String.format(
  1184. resources!!.getString(R.string.nc_lobby_start_date),
  1185. DateUtils.getLocalDateStringFromTimestampForLobby(timestamp)
  1186. )
  1187. val relativeTime = DateUtils.relativeStartTimeForLobby(timestamp, resources!!)
  1188. sb.append("$stringWithStartDate - $relativeTime")
  1189. .append("\n\n")
  1190. }
  1191. sb.append(currentConversation!!.description)
  1192. binding.lobby.lobbyTextView.text = sb.toString()
  1193. } else {
  1194. binding.lobby.lobbyView.visibility = View.GONE
  1195. binding.messagesListView.visibility = View.VISIBLE
  1196. binding.messageInputView.inputEditText?.visibility = View.VISIBLE
  1197. if (isFirstMessagesProcessing && pastPreconditionFailed) {
  1198. pastPreconditionFailed = false
  1199. pullChatMessages(0)
  1200. } else if (futurePreconditionFailed) {
  1201. futurePreconditionFailed = false
  1202. pullChatMessages(1)
  1203. }
  1204. }
  1205. } else {
  1206. binding.lobby.lobbyView.visibility = View.GONE
  1207. binding.messagesListView.visibility = View.VISIBLE
  1208. binding.messageInputView.inputEditText?.visibility = View.VISIBLE
  1209. }
  1210. }
  1211. @Throws(IllegalStateException::class)
  1212. override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
  1213. if (resultCode != RESULT_OK && (requestCode != REQUEST_CODE_MESSAGE_SEARCH)) {
  1214. Log.e(TAG, "resultCode for received intent was != ok")
  1215. return
  1216. }
  1217. when (requestCode) {
  1218. REQUEST_CODE_SELECT_REMOTE_FILES -> {
  1219. val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
  1220. if (pathList?.size!! >= 1) {
  1221. pathList
  1222. .chunked(CHUNK_SIZE)
  1223. .forEach { paths ->
  1224. val data = Data.Builder()
  1225. .putLong(KEY_INTERNAL_USER_ID, conversationUser!!.id!!)
  1226. .putString(KEY_ROOM_TOKEN, roomToken)
  1227. .putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
  1228. .build()
  1229. val worker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
  1230. .setInputData(data)
  1231. .build()
  1232. WorkManager.getInstance().enqueue(worker)
  1233. }
  1234. }
  1235. }
  1236. REQUEST_CODE_CHOOSE_FILE -> {
  1237. try {
  1238. checkNotNull(intent)
  1239. filesToUpload.clear()
  1240. intent.clipData?.let {
  1241. for (index in 0 until it.itemCount) {
  1242. filesToUpload.add(it.getItemAt(index).uri.toString())
  1243. }
  1244. } ?: run {
  1245. checkNotNull(intent.data)
  1246. intent.data.let {
  1247. filesToUpload.add(intent.data.toString())
  1248. }
  1249. }
  1250. require(filesToUpload.isNotEmpty())
  1251. val filenamesWithLineBreaks = StringBuilder("\n")
  1252. for (file in filesToUpload) {
  1253. val filename = FileUtils.getFileName(Uri.parse(file), context)
  1254. filenamesWithLineBreaks.append(filename).append("\n")
  1255. }
  1256. val confirmationQuestion = when (filesToUpload.size) {
  1257. 1 -> context.resources?.getString(R.string.nc_upload_confirm_send_single)?.let {
  1258. String.format(it, title.trim())
  1259. }
  1260. else -> context.resources?.getString(R.string.nc_upload_confirm_send_multiple)?.let {
  1261. String.format(it, title.trim())
  1262. }
  1263. }
  1264. val materialAlertDialogBuilder = MaterialAlertDialogBuilder(binding.messageInputView.context)
  1265. .setTitle(confirmationQuestion)
  1266. .setMessage(filenamesWithLineBreaks.toString())
  1267. .setPositiveButton(R.string.nc_yes) { _, _ ->
  1268. if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
  1269. uploadFiles(filesToUpload)
  1270. } else {
  1271. UploadAndShareFilesWorker.requestStoragePermission(this)
  1272. }
  1273. }
  1274. .setNegativeButton(R.string.nc_no) { _, _ ->
  1275. // unused atm
  1276. }
  1277. viewThemeUtils.dialog.colorMaterialAlertDialogBackground(
  1278. binding.messageInputView.context,
  1279. materialAlertDialogBuilder
  1280. )
  1281. val dialog = materialAlertDialogBuilder.show()
  1282. viewThemeUtils.platform.colorTextButtons(
  1283. dialog.getButton(AlertDialog.BUTTON_POSITIVE),
  1284. dialog.getButton(AlertDialog.BUTTON_NEGATIVE)
  1285. )
  1286. } catch (e: IllegalStateException) {
  1287. Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  1288. .show()
  1289. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1290. } catch (e: IllegalArgumentException) {
  1291. Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
  1292. .show()
  1293. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1294. }
  1295. }
  1296. REQUEST_CODE_SELECT_CONTACT -> {
  1297. val contactUri = intent?.data ?: return
  1298. val cursor: Cursor? = activity?.contentResolver!!.query(contactUri, null, null, null, null)
  1299. if (cursor != null && cursor.moveToFirst()) {
  1300. val id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
  1301. val fileName = ContactUtils.getDisplayNameFromDeviceContact(context, id) + ".vcf"
  1302. val file = File(context.cacheDir, fileName)
  1303. writeContactToVcfFile(cursor, file)
  1304. val shareUri = FileProvider.getUriForFile(
  1305. activity!!,
  1306. BuildConfig.APPLICATION_ID,
  1307. File(file.absolutePath)
  1308. )
  1309. uploadFile(shareUri.toString(), false)
  1310. }
  1311. cursor?.close()
  1312. }
  1313. REQUEST_CODE_PICK_CAMERA -> {
  1314. if (resultCode == RESULT_OK) {
  1315. try {
  1316. filesToUpload.clear()
  1317. if (intent != null && intent.data != null) {
  1318. run {
  1319. intent.data.let {
  1320. filesToUpload.add(intent.data.toString())
  1321. }
  1322. }
  1323. require(filesToUpload.isNotEmpty())
  1324. } else if (videoURI != null) {
  1325. filesToUpload.add(videoURI.toString())
  1326. videoURI = null
  1327. } else {
  1328. throw IllegalStateException("Failed to get data from intent and uri")
  1329. }
  1330. if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
  1331. uploadFiles(filesToUpload)
  1332. } else {
  1333. UploadAndShareFilesWorker.requestStoragePermission(this)
  1334. }
  1335. } catch (e: IllegalStateException) {
  1336. Toast.makeText(
  1337. context,
  1338. context.resources?.getString(R.string.nc_upload_failed),
  1339. Toast.LENGTH_LONG
  1340. )
  1341. .show()
  1342. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1343. } catch (e: IllegalArgumentException) {
  1344. Toast.makeText(
  1345. context,
  1346. context.resources?.getString(R.string.nc_upload_failed),
  1347. Toast.LENGTH_LONG
  1348. )
  1349. .show()
  1350. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1351. }
  1352. }
  1353. }
  1354. REQUEST_CODE_MESSAGE_SEARCH -> {
  1355. val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
  1356. messageId?.let { id ->
  1357. scrollToMessageWithId(id)
  1358. }
  1359. }
  1360. }
  1361. }
  1362. private fun scrollToMessageWithId(messageId: String) {
  1363. val position = adapter?.items?.indexOfFirst {
  1364. it.item is ChatMessage && (it.item as ChatMessage).id == messageId
  1365. }
  1366. if (position != null && position >= 0) {
  1367. binding.messagesListView.smoothScrollToPosition(position)
  1368. } else {
  1369. // TODO show error that we don't have that message?
  1370. }
  1371. }
  1372. private fun writeContactToVcfFile(cursor: Cursor, file: File) {
  1373. val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
  1374. val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
  1375. val fd: AssetFileDescriptor = activity?.contentResolver!!.openAssetFileDescriptor(uri, "r")!!
  1376. fd.use {
  1377. val fis = fd.createInputStream()
  1378. file.createNewFile()
  1379. fis.use { input ->
  1380. file.outputStream().use { output ->
  1381. input.copyTo(output)
  1382. }
  1383. }
  1384. }
  1385. }
  1386. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  1387. if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) {
  1388. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1389. Log.d(ConversationsListController.TAG, "upload starting after permissions were granted")
  1390. if (filesToUpload.isNotEmpty()) {
  1391. uploadFiles(filesToUpload)
  1392. }
  1393. } else {
  1394. Toast
  1395. .makeText(context, context.getString(R.string.read_storage_no_permission), Toast.LENGTH_LONG)
  1396. .show()
  1397. }
  1398. } else if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
  1399. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1400. // do nothing. user will tap on the microphone again if he wants to record audio..
  1401. } else {
  1402. Toast.makeText(
  1403. context,
  1404. context.getString(R.string.nc_voice_message_missing_audio_permission),
  1405. Toast.LENGTH_LONG
  1406. ).show()
  1407. }
  1408. } else if (requestCode == REQUEST_READ_CONTACT_PERMISSION) {
  1409. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1410. val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
  1411. startActivityForResult(intent, REQUEST_CODE_SELECT_CONTACT)
  1412. } else {
  1413. Toast.makeText(
  1414. context,
  1415. context.getString(R.string.nc_share_contact_permission),
  1416. Toast.LENGTH_LONG
  1417. ).show()
  1418. }
  1419. } else if (requestCode == REQUEST_CAMERA_PERMISSION) {
  1420. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1421. Toast
  1422. .makeText(context, context.getString(R.string.camera_permission_granted), Toast.LENGTH_LONG)
  1423. .show()
  1424. } else {
  1425. Toast
  1426. .makeText(context, context.getString(R.string.take_photo_permission), Toast.LENGTH_LONG)
  1427. .show()
  1428. }
  1429. }
  1430. }
  1431. private fun uploadFiles(files: MutableList<String>) {
  1432. for (file in files) {
  1433. uploadFile(file, false)
  1434. }
  1435. }
  1436. private fun uploadFile(fileUri: String, isVoiceMessage: Boolean) {
  1437. var metaData = ""
  1438. if (!participantPermissions.hasChatPermission()) {
  1439. Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
  1440. return
  1441. }
  1442. if (isVoiceMessage) {
  1443. metaData = VOICE_MESSAGE_META_DATA
  1444. }
  1445. try {
  1446. require(fileUri.isNotEmpty())
  1447. UploadAndShareFilesWorker.upload(
  1448. fileUri,
  1449. roomToken!!,
  1450. currentConversation?.displayName!!,
  1451. metaData
  1452. )
  1453. } catch (e: IllegalArgumentException) {
  1454. Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
  1455. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1456. }
  1457. }
  1458. fun sendSelectLocalFileIntent() {
  1459. val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
  1460. type = "*/*"
  1461. addCategory(Intent.CATEGORY_OPENABLE)
  1462. putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
  1463. }
  1464. startActivityForResult(
  1465. Intent.createChooser(
  1466. action,
  1467. context.resources?.getString(
  1468. R.string.nc_upload_choose_local_files
  1469. )
  1470. ),
  1471. REQUEST_CODE_CHOOSE_FILE
  1472. )
  1473. }
  1474. fun sendChooseContactIntent() {
  1475. requestReadContacts()
  1476. }
  1477. fun showBrowserScreen() {
  1478. val sharingFileBrowserIntent = Intent(activity, RemoteFileBrowserActivity::class.java)
  1479. startActivityForResult(sharingFileBrowserIntent, REQUEST_CODE_SELECT_REMOTE_FILES)
  1480. }
  1481. fun showShareLocationScreen() {
  1482. Log.d(TAG, "showShareLocationScreen")
  1483. val bundle = Bundle()
  1484. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  1485. router.pushController(
  1486. RouterTransaction.with(LocationPickerController(bundle))
  1487. .pushChangeHandler(HorizontalChangeHandler())
  1488. .popChangeHandler(HorizontalChangeHandler())
  1489. )
  1490. }
  1491. private fun showConversationInfoScreen() {
  1492. val bundle = Bundle()
  1493. bundle.putParcelable(BundleKeys.KEY_USER_ENTITY, conversationUser)
  1494. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  1495. bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
  1496. router.pushController(
  1497. RouterTransaction.with(ConversationInfoController(bundle))
  1498. .pushChangeHandler(HorizontalChangeHandler())
  1499. .popChangeHandler(HorizontalChangeHandler())
  1500. )
  1501. }
  1502. private fun setupMentionAutocomplete() {
  1503. if (isAlive()) {
  1504. val elevation = MENTION_AUTO_COMPLETE_ELEVATION
  1505. resources?.let {
  1506. val backgroundDrawable = ColorDrawable(it.getColor(R.color.bg_default))
  1507. val presenter = MentionAutocompletePresenter(activity, roomToken)
  1508. val callback = MentionAutocompleteCallback(
  1509. activity,
  1510. conversationUser!!,
  1511. binding.messageInputView.inputEditText,
  1512. viewThemeUtils
  1513. )
  1514. if (mentionAutocomplete == null && binding.messageInputView.inputEditText != null) {
  1515. mentionAutocomplete = Autocomplete.on<Mention>(binding.messageInputView.inputEditText)
  1516. .with(elevation)
  1517. .with(backgroundDrawable)
  1518. .with(MagicCharPolicy('@'))
  1519. .with(presenter)
  1520. .with(callback)
  1521. .build()
  1522. }
  1523. }
  1524. }
  1525. }
  1526. private fun validSessionId(): Boolean {
  1527. return currentConversation != null &&
  1528. !TextUtils.isEmpty(currentConversation?.sessionId) &&
  1529. currentConversation?.sessionId != "0"
  1530. }
  1531. @Suppress("Detekt.TooGenericExceptionCaught")
  1532. override fun onAttach(view: View) {
  1533. super.onAttach(view)
  1534. Log.d(
  1535. TAG,
  1536. "onAttach: Controller: " + System.identityHashCode(this).toString() +
  1537. " Activity: " + System.identityHashCode(activity).toString()
  1538. )
  1539. eventBus.register(this)
  1540. if (conversationUser?.userId != "?" &&
  1541. CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "mention-flag") &&
  1542. activity != null
  1543. ) {
  1544. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener { v -> showConversationInfoScreen() }
  1545. }
  1546. ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId
  1547. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomToken
  1548. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  1549. val smileyButton = binding.messageInputView.findViewById<ImageButton>(R.id.smileyButton)
  1550. emojiPopup = binding.messageInputView.inputEditText?.let {
  1551. EmojiPopup(
  1552. rootView = view,
  1553. editText = it,
  1554. onEmojiPopupShownListener = {
  1555. if (resources != null) {
  1556. smileyButton?.setImageDrawable(
  1557. ContextCompat.getDrawable(context, R.drawable.ic_baseline_keyboard_24)
  1558. )
  1559. }
  1560. },
  1561. onEmojiPopupDismissListener = {
  1562. smileyButton?.setImageDrawable(
  1563. ContextCompat.getDrawable(context, R.drawable.ic_insert_emoticon_black_24dp)
  1564. )
  1565. },
  1566. onEmojiClickListener = {
  1567. try {
  1568. binding.messageInputView.inputEditText?.editableText?.append(" ")
  1569. } catch (npe: NullPointerException) {
  1570. // view binding can be null
  1571. // since this is called asynchronously and UI might have been destroyed in the meantime
  1572. Log.i(WebViewLoginController.TAG, "UI destroyed - view binding already gone")
  1573. }
  1574. }
  1575. )
  1576. }
  1577. smileyButton?.setOnClickListener {
  1578. emojiPopup?.toggle()
  1579. }
  1580. binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.setOnClickListener {
  1581. cancelReply()
  1582. }
  1583. viewThemeUtils.platform
  1584. .themeImageButton(binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton))
  1585. cancelNotificationsForCurrentConversation()
  1586. Log.d(TAG, "onAttach inConversation: $inConversation")
  1587. if (inConversation) {
  1588. Log.d(TAG, "execute joinRoomWithPassword in onAttach")
  1589. joinRoomWithPassword()
  1590. }
  1591. }
  1592. private fun cancelReply() {
  1593. binding.messageInputView.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.visibility = View.GONE
  1594. binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility = View.VISIBLE
  1595. }
  1596. @Suppress("Detekt.TooGenericExceptionCaught")
  1597. private fun cancelNotificationsForCurrentConversation() {
  1598. if (conversationUser != null) {
  1599. if (!TextUtils.isEmpty(roomToken)) {
  1600. try {
  1601. NotificationUtils.cancelExistingNotificationsForRoom(
  1602. applicationContext,
  1603. conversationUser,
  1604. roomToken!!
  1605. )
  1606. } catch (e: RuntimeException) {
  1607. Log.w(TAG, "Cancel notifications for current conversation results with an error.", e)
  1608. }
  1609. }
  1610. }
  1611. }
  1612. override fun onDetach(view: View) {
  1613. super.onDetach(view)
  1614. Log.d(
  1615. TAG,
  1616. "onDetach: Controller: " + System.identityHashCode(this).toString() +
  1617. " Activity: " + System.identityHashCode(activity).toString()
  1618. )
  1619. eventBus.unregister(this)
  1620. if (activity != null) {
  1621. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  1622. }
  1623. checkingLobbyStatus = false
  1624. if (lobbyTimerHandler != null) {
  1625. lobbyTimerHandler?.removeCallbacksAndMessages(null)
  1626. }
  1627. if (conversationUser != null && isActivityNotChangingConfigurations() && isNotInCall()) {
  1628. ApplicationWideCurrentRoomHolder.getInstance().clear()
  1629. if (inConversation && validSessionId()) {
  1630. leaveRoom()
  1631. }
  1632. }
  1633. if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
  1634. mentionAutocomplete?.dismissPopup()
  1635. }
  1636. }
  1637. private fun isActivityNotChangingConfigurations(): Boolean {
  1638. return activity != null && !activity?.isChangingConfigurations!!
  1639. }
  1640. private fun isNotInCall(): Boolean {
  1641. return !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
  1642. !ApplicationWideCurrentRoomHolder.getInstance().isDialing
  1643. }
  1644. override val title: String
  1645. get() =
  1646. if (currentConversation?.displayName != null) {
  1647. try {
  1648. " " + EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
  1649. } catch (e: IllegalStateException) {
  1650. " " + currentConversation?.displayName
  1651. }
  1652. } else {
  1653. ""
  1654. }
  1655. public override fun onDestroy() {
  1656. super.onDestroy()
  1657. if (activity != null) {
  1658. activity?.findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  1659. }
  1660. if (actionBar != null) {
  1661. actionBar?.setIcon(null)
  1662. }
  1663. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  1664. adapter = null
  1665. inConversation = false
  1666. }
  1667. private fun joinRoomWithPassword() {
  1668. Log.d(
  1669. TAG,
  1670. "joinRoomWithPassword start: " + (currentConversation == null).toString() +
  1671. " sessionId: " + currentConversation?.sessionId
  1672. )
  1673. if (!validSessionId()) {
  1674. var apiVersion = 1
  1675. // FIXME Fix API checking with guests?
  1676. if (conversationUser != null) {
  1677. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1678. }
  1679. val startNanoTime = System.nanoTime()
  1680. Log.d(TAG, "joinRoomWithPassword - joinRoom - calling: $startNanoTime")
  1681. ncApi.joinRoom(
  1682. credentials,
  1683. ApiUtils.getUrlForParticipantsActive(apiVersion, conversationUser?.baseUrl, roomToken),
  1684. roomPassword
  1685. )
  1686. ?.subscribeOn(Schedulers.io())
  1687. ?.observeOn(AndroidSchedulers.mainThread())
  1688. ?.retry(RETRIES)
  1689. ?.subscribe(object : Observer<RoomOverall> {
  1690. override fun onSubscribe(d: Disposable) {
  1691. disposables.add(d)
  1692. }
  1693. @Suppress("Detekt.TooGenericExceptionCaught")
  1694. override fun onNext(roomOverall: RoomOverall) {
  1695. Log.d(TAG, "joinRoomWithPassword - joinRoom - got response: $startNanoTime")
  1696. inConversation = true
  1697. currentConversation?.sessionId = roomOverall.ocs!!.data!!.sessionId
  1698. Log.d(TAG, "joinRoomWithPassword - sessionId: " + currentConversation?.sessionId)
  1699. ApplicationWideCurrentRoomHolder.getInstance().session =
  1700. currentConversation?.sessionId
  1701. setupWebsocket()
  1702. try {
  1703. checkLobbyState()
  1704. } catch (npe: NullPointerException) {
  1705. // view binding can be null
  1706. // since this is called asynchronously and UI might have been destroyed in the meantime
  1707. Log.i(TAG, "UI destroyed - view binding already gone")
  1708. }
  1709. if (isFirstMessagesProcessing) {
  1710. pullChatMessages(0)
  1711. } else {
  1712. pullChatMessages(1, 0)
  1713. }
  1714. if (magicWebSocketInstance != null) {
  1715. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1716. roomToken,
  1717. currentConversation?.sessionId
  1718. )
  1719. }
  1720. if (startCallFromNotification != null && startCallFromNotification ?: false) {
  1721. startCallFromNotification = false
  1722. startACall(voiceOnly, false)
  1723. }
  1724. }
  1725. override fun onError(e: Throwable) {
  1726. Log.e(TAG, "joinRoomWithPassword - joinRoom - ERROR", e)
  1727. }
  1728. override fun onComplete() {
  1729. // unused atm
  1730. }
  1731. })
  1732. } else {
  1733. inConversation = true
  1734. ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation?.sessionId
  1735. if (magicWebSocketInstance != null) {
  1736. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1737. roomToken,
  1738. currentConversation?.sessionId
  1739. )
  1740. }
  1741. checkLobbyState()
  1742. if (isFirstMessagesProcessing) {
  1743. pullChatMessages(0)
  1744. } else {
  1745. pullChatMessages(1)
  1746. }
  1747. }
  1748. }
  1749. private fun leaveRoom() {
  1750. Log.d(TAG, "leaveRoom")
  1751. var apiVersion = 1
  1752. // FIXME Fix API checking with guests?
  1753. if (conversationUser != null) {
  1754. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  1755. }
  1756. val startNanoTime = System.nanoTime()
  1757. Log.d(TAG, "leaveRoom - leaveRoom - calling: $startNanoTime")
  1758. ncApi.leaveRoom(
  1759. credentials,
  1760. ApiUtils.getUrlForParticipantsActive(
  1761. apiVersion,
  1762. conversationUser?.baseUrl,
  1763. roomToken
  1764. )
  1765. )
  1766. ?.subscribeOn(Schedulers.io())
  1767. ?.observeOn(AndroidSchedulers.mainThread())
  1768. ?.subscribe(object : Observer<GenericOverall> {
  1769. override fun onSubscribe(d: Disposable) {
  1770. disposables.add(d)
  1771. }
  1772. override fun onNext(genericOverall: GenericOverall) {
  1773. Log.d(TAG, "leaveRoom - leaveRoom - got response: $startNanoTime")
  1774. checkingLobbyStatus = false
  1775. if (lobbyTimerHandler != null) {
  1776. lobbyTimerHandler?.removeCallbacksAndMessages(null)
  1777. }
  1778. if (magicWebSocketInstance != null && currentConversation != null) {
  1779. magicWebSocketInstance?.joinRoomWithRoomTokenAndSession(
  1780. "",
  1781. currentConversation?.sessionId
  1782. )
  1783. } else {
  1784. Log.e(TAG, "magicWebSocketInstance or currentConversation were null! Failed to leave the room!")
  1785. }
  1786. currentConversation?.sessionId = "0"
  1787. }
  1788. override fun onError(e: Throwable) {
  1789. Log.e(TAG, "leaveRoom - leaveRoom - ERROR", e)
  1790. }
  1791. override fun onComplete() {
  1792. Log.d(TAG, "leaveRoom - leaveRoom - completed: $startNanoTime")
  1793. disposables.dispose()
  1794. }
  1795. })
  1796. }
  1797. private fun submitMessage(sendWithoutNotification: Boolean) {
  1798. if (binding.messageInputView.inputEditText != null) {
  1799. val editable = binding.messageInputView.inputEditText!!.editableText
  1800. val mentionSpans = editable.getSpans(
  1801. 0,
  1802. editable.length,
  1803. Spans.MentionChipSpan::class.java
  1804. )
  1805. var mentionSpan: Spans.MentionChipSpan
  1806. for (i in mentionSpans.indices) {
  1807. mentionSpan = mentionSpans[i]
  1808. var mentionId = mentionSpan.id
  1809. if (mentionId.contains(" ") || mentionId.startsWith("guest/")) {
  1810. mentionId = "\"" + mentionId + "\""
  1811. }
  1812. editable.replace(editable.getSpanStart(mentionSpan), editable.getSpanEnd(mentionSpan), "@$mentionId")
  1813. }
  1814. binding.messageInputView.inputEditText?.setText("")
  1815. val replyMessageId: Int? = view?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)?.tag as Int?
  1816. sendMessage(
  1817. editable,
  1818. if (
  1819. view
  1820. ?.findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  1821. ?.visibility == View.VISIBLE
  1822. ) replyMessageId else null,
  1823. sendWithoutNotification
  1824. )
  1825. cancelReply()
  1826. }
  1827. }
  1828. private fun sendMessage(message: CharSequence, replyTo: Int?, sendWithoutNotification: Boolean) {
  1829. if (conversationUser != null) {
  1830. val apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1831. ncApi.sendChatMessage(
  1832. credentials,
  1833. ApiUtils.getUrlForChat(apiVersion, conversationUser.baseUrl, roomToken),
  1834. message,
  1835. conversationUser.displayName,
  1836. replyTo,
  1837. sendWithoutNotification
  1838. )
  1839. ?.subscribeOn(Schedulers.io())
  1840. ?.observeOn(AndroidSchedulers.mainThread())
  1841. ?.subscribe(object : Observer<GenericOverall> {
  1842. override fun onSubscribe(d: Disposable) {
  1843. // unused atm
  1844. }
  1845. @Suppress("Detekt.TooGenericExceptionCaught")
  1846. override fun onNext(genericOverall: GenericOverall) {
  1847. myFirstMessage = message
  1848. try {
  1849. if (binding.popupBubbleView.isShown) {
  1850. binding.popupBubbleView.hide()
  1851. }
  1852. binding.messagesListView.smoothScrollToPosition(0)
  1853. } catch (npe: NullPointerException) {
  1854. // view binding can be null
  1855. // since this is called asynchronously and UI might have been destroyed in the meantime
  1856. Log.i(TAG, "UI destroyed - view binding already gone")
  1857. }
  1858. }
  1859. override fun onError(e: Throwable) {
  1860. if (e is HttpException) {
  1861. val code = e.code()
  1862. if (code.toString().startsWith("2")) {
  1863. myFirstMessage = message
  1864. if (binding.popupBubbleView.isShown) {
  1865. binding.popupBubbleView.hide()
  1866. }
  1867. binding.messagesListView.smoothScrollToPosition(0)
  1868. }
  1869. }
  1870. }
  1871. override fun onComplete() {
  1872. // unused atm
  1873. }
  1874. })
  1875. }
  1876. showMicrophoneButton(true)
  1877. }
  1878. private fun setupWebsocket() {
  1879. if (conversationUser != null) {
  1880. magicWebSocketInstance =
  1881. if (WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id!!) != null) {
  1882. WebSocketConnectionHelper.getMagicWebSocketInstanceForUserId(conversationUser.id!!)
  1883. } else {
  1884. Log.d(TAG, "magicWebSocketInstance became null")
  1885. null
  1886. }
  1887. }
  1888. }
  1889. fun pullChatMessages(lookIntoFuture: Int, setReadMarker: Int = 1, xChatLastCommonRead: Int? = null) {
  1890. if (!inConversation) {
  1891. return
  1892. }
  1893. if (pullChatMessagesPending) {
  1894. // Sometimes pullChatMessages may be called before response to a previous call is received.
  1895. // In such cases just ignore the second call. Message processing will continue when response to the
  1896. // earlier call is received.
  1897. // More details: https://github.com/nextcloud/talk-android/pull/1766
  1898. Log.d(TAG, "pullChatMessages - pullChatMessagesPending is true, exiting")
  1899. return
  1900. }
  1901. pullChatMessagesPending = true
  1902. val fieldMap = HashMap<String, Int>()
  1903. fieldMap["includeLastKnown"] = 0
  1904. if (lookIntoFuture > 0) {
  1905. lookingIntoFuture = true
  1906. } else if (isFirstMessagesProcessing) {
  1907. if (currentConversation != null) {
  1908. globalLastKnownFutureMessageId = currentConversation!!.lastReadMessage
  1909. globalLastKnownPastMessageId = currentConversation!!.lastReadMessage
  1910. fieldMap["includeLastKnown"] = 1
  1911. }
  1912. }
  1913. val timeout = if (lookingIntoFuture) {
  1914. LOOKING_INTO_FUTURE_TIMEOUT
  1915. } else {
  1916. 0
  1917. }
  1918. fieldMap["timeout"] = timeout
  1919. fieldMap["lookIntoFuture"] = lookIntoFuture
  1920. fieldMap["limit"] = MESSAGE_PULL_LIMIT
  1921. fieldMap["setReadMarker"] = setReadMarker
  1922. val lastKnown = if (lookIntoFuture > 0) {
  1923. globalLastKnownFutureMessageId
  1924. } else {
  1925. globalLastKnownPastMessageId
  1926. }
  1927. fieldMap["lastKnownMessageId"] = lastKnown
  1928. xChatLastCommonRead?.let {
  1929. fieldMap["lastCommonReadId"] = it
  1930. }
  1931. var apiVersion = 1
  1932. // FIXME this is a best guess, guests would need to get the capabilities themselves
  1933. if (conversationUser != null) {
  1934. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  1935. }
  1936. if (lookIntoFuture > 0) {
  1937. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - calling")
  1938. ncApi.pullChatMessages(
  1939. credentials,
  1940. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken),
  1941. fieldMap
  1942. )
  1943. ?.subscribeOn(Schedulers.io())
  1944. ?.observeOn(AndroidSchedulers.mainThread())
  1945. ?.subscribe(object : Observer<Response<*>> {
  1946. override fun onSubscribe(d: Disposable) {
  1947. disposables.add(d)
  1948. }
  1949. @Suppress("Detekt.TooGenericExceptionCaught")
  1950. override fun onNext(response: Response<*>) {
  1951. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - got response")
  1952. pullChatMessagesPending = false
  1953. try {
  1954. if (response.code() == HTTP_CODE_NOT_MODIFIED) {
  1955. Log.d(TAG, "pullChatMessages - quasi recursive call to pullChatMessages")
  1956. pullChatMessages(1, setReadMarker, xChatLastCommonRead)
  1957. } else if (response.code() == HTTP_CODE_PRECONDITION_FAILED) {
  1958. futurePreconditionFailed = true
  1959. } else {
  1960. processMessagesResponse(response, true)
  1961. }
  1962. } catch (npe: NullPointerException) {
  1963. // view binding can be null
  1964. // since this is called asynchronously and UI might have been destroyed in the meantime
  1965. Log.i(TAG, "UI destroyed - view binding already gone")
  1966. }
  1967. processExpiredMessages()
  1968. }
  1969. override fun onError(e: Throwable) {
  1970. Log.e(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture > 0] - ERROR", e)
  1971. pullChatMessagesPending = false
  1972. }
  1973. override fun onComplete() {
  1974. pullChatMessagesPending = false
  1975. }
  1976. })
  1977. } else {
  1978. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - calling")
  1979. ncApi.pullChatMessages(
  1980. credentials,
  1981. ApiUtils.getUrlForChat(apiVersion, conversationUser?.baseUrl, roomToken),
  1982. fieldMap
  1983. )
  1984. ?.subscribeOn(Schedulers.io())
  1985. ?.observeOn(AndroidSchedulers.mainThread())
  1986. ?.subscribe(object : Observer<Response<*>> {
  1987. override fun onSubscribe(d: Disposable) {
  1988. disposables.add(d)
  1989. }
  1990. @Suppress("Detekt.TooGenericExceptionCaught")
  1991. override fun onNext(response: Response<*>) {
  1992. Log.d(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - got response")
  1993. pullChatMessagesPending = false
  1994. try {
  1995. if (response.code() == HTTP_CODE_PRECONDITION_FAILED) {
  1996. pastPreconditionFailed = true
  1997. } else {
  1998. processMessagesResponse(response, false)
  1999. }
  2000. } catch (e: NullPointerException) {
  2001. // view binding can be null
  2002. // since this is called asynchronously and UI might have been destroyed in the meantime
  2003. Log.i(TAG, "UI destroyed - view binding already gone", e)
  2004. }
  2005. processExpiredMessages()
  2006. }
  2007. override fun onError(e: Throwable) {
  2008. Log.e(TAG, "pullChatMessages - pullChatMessages[lookIntoFuture <= 0] - ERROR", e)
  2009. pullChatMessagesPending = false
  2010. }
  2011. override fun onComplete() {
  2012. pullChatMessagesPending = false
  2013. }
  2014. })
  2015. }
  2016. }
  2017. private fun processExpiredMessages() {
  2018. fun deleteExpiredMessages() {
  2019. val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
  2020. val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
  2021. if (adapter?.items != null) {
  2022. for (itemWrapper in adapter?.items!!) {
  2023. if (itemWrapper.item is ChatMessage) {
  2024. val chatMessage = itemWrapper.item as ChatMessage
  2025. if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) {
  2026. messagesToDelete.add(chatMessage)
  2027. }
  2028. }
  2029. }
  2030. adapter!!.delete(messagesToDelete)
  2031. adapter!!.notifyDataSetChanged()
  2032. }
  2033. }
  2034. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "message-expiration")) {
  2035. deleteExpiredMessages()
  2036. }
  2037. }
  2038. private fun processMessagesResponse(response: Response<*>, isFromTheFuture: Boolean) {
  2039. val xChatLastCommonRead = response.headers()["X-Chat-Last-Common-Read"]?.let {
  2040. Integer.parseInt(it)
  2041. }
  2042. processHeaderChatLastGiven(response, isFromTheFuture)
  2043. if (response.code() == HTTP_CODE_OK) {
  2044. val chatOverall = response.body() as ChatOverall?
  2045. val chatMessageList = handleSystemMessages(chatOverall?.ocs!!.data!!)
  2046. processMessages(chatMessageList, isFromTheFuture, xChatLastCommonRead)
  2047. } else if (response.code() == HTTP_CODE_NOT_MODIFIED && !isFromTheFuture) {
  2048. if (isFirstMessagesProcessing) {
  2049. cancelNotificationsForCurrentConversation()
  2050. isFirstMessagesProcessing = false
  2051. binding.progressBar.visibility = View.GONE
  2052. }
  2053. historyRead = true
  2054. if (!lookingIntoFuture && inConversation) {
  2055. pullChatMessages(1)
  2056. }
  2057. }
  2058. }
  2059. private fun processMessages(
  2060. chatMessageList: List<ChatMessage>,
  2061. isFromTheFuture: Boolean,
  2062. xChatLastCommonRead: Int?
  2063. ) {
  2064. if (chatMessageList.isNotEmpty() &&
  2065. ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType
  2066. ) {
  2067. adapter?.clear()
  2068. adapter?.notifyDataSetChanged()
  2069. }
  2070. if (isFirstMessagesProcessing) {
  2071. cancelNotificationsForCurrentConversation()
  2072. isFirstMessagesProcessing = false
  2073. binding.progressBar.visibility = View.GONE
  2074. binding.messagesListView.visibility = View.VISIBLE
  2075. }
  2076. if (isFromTheFuture) {
  2077. processMessagesFromTheFuture(chatMessageList)
  2078. } else {
  2079. processMessagesNotFromTheFuture(chatMessageList)
  2080. }
  2081. updateReadStatusOfAllMessages(xChatLastCommonRead)
  2082. adapter?.notifyDataSetChanged()
  2083. if (inConversation) {
  2084. pullChatMessages(1, 1, xChatLastCommonRead)
  2085. }
  2086. }
  2087. private fun updateReadStatusOfAllMessages(xChatLastCommonRead: Int?) {
  2088. for (message in adapter!!.items) {
  2089. xChatLastCommonRead?.let {
  2090. updateReadStatusOfMessage(message, it)
  2091. }
  2092. }
  2093. }
  2094. private fun updateReadStatusOfMessage(
  2095. message: MessagesListAdapter<IMessage>.Wrapper<Any>,
  2096. xChatLastCommonRead: Int
  2097. ) {
  2098. if (message.item is ChatMessage) {
  2099. val chatMessage = message.item as ChatMessage
  2100. if (chatMessage.jsonMessageId <= xChatLastCommonRead) {
  2101. chatMessage.readStatus = ReadStatus.READ
  2102. } else {
  2103. chatMessage.readStatus = ReadStatus.SENT
  2104. }
  2105. }
  2106. }
  2107. private fun processMessagesFromTheFuture(chatMessageList: List<ChatMessage>) {
  2108. val shouldAddNewMessagesNotice = (adapter?.itemCount ?: 0) > 0 && chatMessageList.isNotEmpty()
  2109. if (shouldAddNewMessagesNotice) {
  2110. val unreadChatMessage = ChatMessage()
  2111. unreadChatMessage.jsonMessageId = -1
  2112. unreadChatMessage.actorId = "-1"
  2113. unreadChatMessage.timestamp = chatMessageList[0].timestamp
  2114. unreadChatMessage.message = context.getString(R.string.nc_new_messages)
  2115. adapter?.addToStart(unreadChatMessage, false)
  2116. }
  2117. determinePreviousMessageIds(chatMessageList)
  2118. addMessagesToAdapter(shouldAddNewMessagesNotice, chatMessageList)
  2119. if (shouldAddNewMessagesNotice && adapter != null) {
  2120. layoutManager?.scrollToPositionWithOffset(
  2121. adapter!!.getMessagePositionByIdInReverse("-1"),
  2122. binding.messagesListView.height / 2
  2123. )
  2124. }
  2125. }
  2126. private fun addMessagesToAdapter(
  2127. shouldAddNewMessagesNotice: Boolean,
  2128. chatMessageList: List<ChatMessage>
  2129. ) {
  2130. val isThereANewNotice =
  2131. shouldAddNewMessagesNotice || adapter?.getMessagePositionByIdInReverse("-1") != -1
  2132. for (chatMessage in chatMessageList) {
  2133. chatMessage.activeUser = conversationUser
  2134. val shouldScroll =
  2135. !isThereANewNotice &&
  2136. !shouldAddNewMessagesNotice &&
  2137. layoutManager?.findFirstVisibleItemPosition() == 0 ||
  2138. adapter != null &&
  2139. adapter?.itemCount == 0
  2140. modifyMessageCount(shouldAddNewMessagesNotice, shouldScroll)
  2141. adapter?.let {
  2142. chatMessage.isGrouped = (
  2143. it.isPreviousSameAuthor(
  2144. chatMessage.actorId,
  2145. -1
  2146. ) && it.getSameAuthorLastMessagesCount(chatMessage.actorId) %
  2147. GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD > 0
  2148. )
  2149. chatMessage.isOneToOneConversation =
  2150. (currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
  2151. it.addToStart(chatMessage, shouldScroll)
  2152. }
  2153. }
  2154. }
  2155. private fun modifyMessageCount(shouldAddNewMessagesNotice: Boolean, shouldScroll: Boolean) {
  2156. if (!shouldAddNewMessagesNotice && !shouldScroll) {
  2157. if (!binding.popupBubbleView.isShown) {
  2158. newMessagesCount = 1
  2159. binding.popupBubbleView.show()
  2160. } else if (binding.popupBubbleView.isShown) {
  2161. newMessagesCount++
  2162. }
  2163. } else {
  2164. newMessagesCount = 0
  2165. }
  2166. }
  2167. private fun processMessagesNotFromTheFuture(chatMessageList: List<ChatMessage>) {
  2168. var countGroupedMessages = 0
  2169. determinePreviousMessageIds(chatMessageList)
  2170. for (i in chatMessageList.indices) {
  2171. if (chatMessageList.size > i + 1) {
  2172. if (isSameDayNonSystemMessages(chatMessageList[i], chatMessageList[i + 1]) &&
  2173. chatMessageList[i + 1].actorId == chatMessageList[i].actorId &&
  2174. countGroupedMessages < GROUPED_MESSAGES_THRESHOLD
  2175. ) {
  2176. chatMessageList[i].isGrouped = true
  2177. countGroupedMessages++
  2178. } else {
  2179. countGroupedMessages = 0
  2180. }
  2181. }
  2182. val chatMessage = chatMessageList[i]
  2183. chatMessage.isOneToOneConversation =
  2184. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2185. chatMessage.activeUser = conversationUser
  2186. }
  2187. if (adapter != null) {
  2188. adapter?.addToEnd(chatMessageList, false)
  2189. }
  2190. scrollToRequestedMessageIfNeeded()
  2191. }
  2192. private fun determinePreviousMessageIds(chatMessageList: List<ChatMessage>) {
  2193. var previousMessageId = NO_PREVIOUS_MESSAGE_ID
  2194. for (i in chatMessageList.indices.reversed()) {
  2195. val chatMessage = chatMessageList[i]
  2196. if (previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2197. chatMessage.previousMessageId = previousMessageId
  2198. } else if (adapter?.isEmpty != true) {
  2199. if (adapter!!.items[0].item is ChatMessage) {
  2200. chatMessage.previousMessageId = (adapter!!.items[0].item as ChatMessage).jsonMessageId
  2201. } else if (adapter!!.items.size > 1 && adapter!!.items[1].item is ChatMessage) {
  2202. chatMessage.previousMessageId = (adapter!!.items[1].item as ChatMessage).jsonMessageId
  2203. }
  2204. }
  2205. previousMessageId = chatMessage.jsonMessageId
  2206. }
  2207. }
  2208. private fun processHeaderChatLastGiven(response: Response<*>, isFromTheFuture: Boolean) {
  2209. val xChatLastGivenHeader: String? = response.headers()["X-Chat-Last-Given"]
  2210. val header = if (response.headers().size > 0 &&
  2211. xChatLastGivenHeader?.isNotEmpty() == true
  2212. ) {
  2213. xChatLastGivenHeader.toInt()
  2214. } else {
  2215. return
  2216. }
  2217. if (header > 0) {
  2218. if (isFromTheFuture) {
  2219. globalLastKnownFutureMessageId = header
  2220. } else {
  2221. if (globalLastKnownFutureMessageId == -1) {
  2222. globalLastKnownFutureMessageId = header
  2223. }
  2224. globalLastKnownPastMessageId = header
  2225. }
  2226. }
  2227. }
  2228. private fun scrollToRequestedMessageIfNeeded() {
  2229. args.getString(BundleKeys.KEY_MESSAGE_ID)?.let {
  2230. scrollToMessageWithId(it)
  2231. }
  2232. }
  2233. private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean {
  2234. return TextUtils.isEmpty(messageLeft.systemMessage) &&
  2235. TextUtils.isEmpty(messageRight.systemMessage) &&
  2236. DateFormatter.isSameDay(messageLeft.createdAt, messageRight.createdAt)
  2237. }
  2238. override fun onLoadMore(page: Int, totalItemsCount: Int) {
  2239. if (!historyRead && inConversation) {
  2240. pullChatMessages(0)
  2241. }
  2242. }
  2243. override fun format(date: Date): String {
  2244. return if (DateFormatter.isToday(date)) {
  2245. resources!!.getString(R.string.nc_date_header_today)
  2246. } else if (DateFormatter.isYesterday(date)) {
  2247. resources!!.getString(R.string.nc_date_header_yesterday)
  2248. } else {
  2249. DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
  2250. }
  2251. }
  2252. override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
  2253. super.onCreateOptionsMenu(menu, inflater)
  2254. inflater.inflate(R.menu.menu_conversation, menu)
  2255. viewThemeUtils.platform.colorToolbarMenuIcon(
  2256. binding.messageInputView.context,
  2257. menu.findItem(R.id.conversation_voice_call)
  2258. )
  2259. viewThemeUtils.platform.colorToolbarMenuIcon(
  2260. binding.messageInputView.context,
  2261. menu.findItem(R.id.conversation_video_call)
  2262. )
  2263. if (conversationUser?.userId == "?") {
  2264. menu.removeItem(R.id.conversation_info)
  2265. } else {
  2266. conversationInfoMenuItem = menu.findItem(R.id.conversation_info)
  2267. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "rich-object-list-media")) {
  2268. conversationSharedItemsItem = menu.findItem(R.id.shared_items)
  2269. } else {
  2270. menu.removeItem(R.id.shared_items)
  2271. }
  2272. loadAvatarForStatusBar()
  2273. }
  2274. if (CapabilitiesUtilNew.isAbleToCall(conversationUser)) {
  2275. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  2276. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  2277. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "silent-call")) {
  2278. Handler().post {
  2279. activity?.findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
  2280. showCallButtonMenu(true)
  2281. true
  2282. }
  2283. }
  2284. Handler().post {
  2285. activity?.findViewById<View?>(R.id.conversation_video_call)?.setOnLongClickListener {
  2286. showCallButtonMenu(false)
  2287. true
  2288. }
  2289. }
  2290. }
  2291. } else {
  2292. menu.removeItem(R.id.conversation_video_call)
  2293. menu.removeItem(R.id.conversation_voice_call)
  2294. }
  2295. }
  2296. override fun onPrepareOptionsMenu(menu: Menu) {
  2297. super.onPrepareOptionsMenu(menu)
  2298. conversationUser?.let {
  2299. if (CapabilitiesUtilNew.hasSpreedFeatureCapability(it, "read-only-rooms")) {
  2300. checkShowCallButtons()
  2301. }
  2302. val searchItem = menu.findItem(R.id.conversation_search)
  2303. searchItem.isVisible = CapabilitiesUtilNew.isUnifiedSearchAvailable(it)
  2304. }
  2305. }
  2306. override fun onOptionsItemSelected(item: MenuItem): Boolean {
  2307. return when (item.itemId) {
  2308. android.R.id.home -> {
  2309. (activity as MainActivity).resetConversationsList()
  2310. true
  2311. }
  2312. R.id.conversation_video_call -> {
  2313. startACall(false, false)
  2314. true
  2315. }
  2316. R.id.conversation_voice_call -> {
  2317. startACall(true, false)
  2318. true
  2319. }
  2320. R.id.conversation_info -> {
  2321. showConversationInfoScreen()
  2322. true
  2323. }
  2324. R.id.shared_items -> {
  2325. showSharedItems()
  2326. true
  2327. }
  2328. R.id.conversation_search -> {
  2329. startMessageSearch()
  2330. true
  2331. }
  2332. else -> super.onOptionsItemSelected(item)
  2333. }
  2334. }
  2335. private fun showSharedItems() {
  2336. val intent = Intent(activity, SharedItemsActivity::class.java)
  2337. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2338. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2339. intent.putExtra(KEY_USER_ENTITY, conversationUser as Parcelable)
  2340. intent.putExtra(
  2341. SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR,
  2342. currentConversation?.isParticipantOwnerOrModerator
  2343. )
  2344. activity!!.startActivity(intent)
  2345. }
  2346. private fun startMessageSearch() {
  2347. val intent = Intent(activity, MessageSearchActivity::class.java)
  2348. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2349. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2350. startActivityForResult(intent, REQUEST_CODE_MESSAGE_SEARCH)
  2351. }
  2352. private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  2353. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  2354. val chatMessageIterator = chatMessageMap.iterator()
  2355. while (chatMessageIterator.hasNext()) {
  2356. val currentMessage = chatMessageIterator.next()
  2357. // setDeletionFlagsAndRemoveInfomessages
  2358. if (isInfoMessageAboutDeletion(currentMessage)) {
  2359. if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) {
  2360. // if chatMessageMap doesn't contain message to delete (this happens when lookingIntoFuture),
  2361. // the message to delete has to be modified directly inside the adapter
  2362. setMessageAsDeleted(currentMessage.value.parentMessage)
  2363. } else {
  2364. chatMessageMap[currentMessage.value.parentMessage!!.id]!!.isDeleted = true
  2365. }
  2366. chatMessageIterator.remove()
  2367. }
  2368. // delete reactions system messages
  2369. else if (isReactionsMessage(currentMessage)) {
  2370. if (!chatMessageMap.containsKey(currentMessage.value.parentMessage!!.id)) {
  2371. updateAdapterForReaction(currentMessage.value.parentMessage)
  2372. }
  2373. chatMessageIterator.remove()
  2374. }
  2375. // delete poll system messages
  2376. else if (isPollVotedMessage(currentMessage)) {
  2377. chatMessageIterator.remove()
  2378. }
  2379. }
  2380. return chatMessageMap.values.toList()
  2381. }
  2382. private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2383. return currentMessage.value.parentMessage != null && currentMessage.value.systemMessageType == ChatMessage
  2384. .SystemMessageType.MESSAGE_DELETED
  2385. }
  2386. private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2387. return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION ||
  2388. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
  2389. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
  2390. }
  2391. private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean {
  2392. return currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED
  2393. }
  2394. private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
  2395. val pp = ParticipantPermissions(conversationUser!!, currentConversation!!)
  2396. if (!pp.canStartCall() && currentConversation?.hasCall == false) {
  2397. Toast.makeText(context, R.string.startCallForbidden, Toast.LENGTH_LONG).show()
  2398. } else {
  2399. ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
  2400. val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
  2401. if (callIntent != null) {
  2402. startActivity(callIntent)
  2403. }
  2404. }
  2405. }
  2406. private fun getIntentForCall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean): Intent? {
  2407. currentConversation?.let {
  2408. val bundle = Bundle()
  2409. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  2410. bundle.putString(KEY_ROOM_ID, roomId)
  2411. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  2412. bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
  2413. bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl)
  2414. bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
  2415. bundle.putBoolean(
  2416. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
  2417. participantPermissions.canPublishAudio()
  2418. )
  2419. bundle.putBoolean(
  2420. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
  2421. participantPermissions.canPublishVideo()
  2422. )
  2423. if (isVoiceOnlyCall) {
  2424. bundle.putBoolean(BundleKeys.KEY_CALL_VOICE_ONLY, true)
  2425. }
  2426. if (callWithoutNotification) {
  2427. bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true)
  2428. }
  2429. return if (activity != null) {
  2430. val callIntent = Intent(activity, CallActivity::class.java)
  2431. callIntent.putExtras(bundle)
  2432. callIntent
  2433. } else {
  2434. null
  2435. }
  2436. } ?: run {
  2437. return null
  2438. }
  2439. }
  2440. override fun onClickReactions(chatMessage: ChatMessage) {
  2441. activity?.let {
  2442. ShowReactionsDialog(
  2443. activity!!,
  2444. currentConversation,
  2445. chatMessage,
  2446. conversationUser,
  2447. participantPermissions.hasChatPermission(),
  2448. ncApi
  2449. ).show()
  2450. }
  2451. }
  2452. override fun onOpenMessageActionsDialog(chatMessage: ChatMessage) {
  2453. openMessageActionsDialog(chatMessage)
  2454. }
  2455. override fun onMessageViewLongClick(view: View?, message: IMessage?) {
  2456. openMessageActionsDialog(message)
  2457. }
  2458. override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
  2459. onOpenMessageActionsDialog(chatMessage)
  2460. }
  2461. private fun openMessageActionsDialog(iMessage: IMessage?) {
  2462. val message = iMessage as ChatMessage
  2463. if (hasVisibleItems(message) && !isSystemMessage(message)) {
  2464. activity?.let {
  2465. MessageActionsDialog(
  2466. this,
  2467. message,
  2468. conversationUser,
  2469. currentConversation,
  2470. isShowMessageDeletionButton(message),
  2471. participantPermissions.hasChatPermission(),
  2472. ncApi
  2473. ).show()
  2474. }
  2475. }
  2476. }
  2477. private fun isSystemMessage(message: ChatMessage): Boolean {
  2478. return ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType()
  2479. }
  2480. fun deleteMessage(message: IMessage?) {
  2481. if (!participantPermissions.hasChatPermission()) {
  2482. Log.w(
  2483. TAG,
  2484. "Deletion of message is skipped because of restrictions by permissions. " +
  2485. "This method should not have been called!"
  2486. )
  2487. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  2488. } else {
  2489. var apiVersion = 1
  2490. // FIXME Fix API checking with guests?
  2491. if (conversationUser != null) {
  2492. apiVersion = ApiUtils.getChatApiVersion(conversationUser, intArrayOf(1))
  2493. }
  2494. ncApi.deleteChatMessage(
  2495. credentials,
  2496. ApiUtils.getUrlForChatMessage(
  2497. apiVersion,
  2498. conversationUser?.baseUrl,
  2499. roomToken,
  2500. message?.id
  2501. )
  2502. )?.subscribeOn(Schedulers.io())
  2503. ?.observeOn(AndroidSchedulers.mainThread())
  2504. ?.subscribe(object : Observer<ChatOverallSingleMessage> {
  2505. override fun onSubscribe(d: Disposable) {
  2506. // unused atm
  2507. }
  2508. override fun onNext(t: ChatOverallSingleMessage) {
  2509. if (t.ocs!!.meta!!.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
  2510. Toast.makeText(
  2511. context,
  2512. R.string.nc_delete_message_leaked_to_matterbridge,
  2513. Toast.LENGTH_LONG
  2514. ).show()
  2515. }
  2516. }
  2517. override fun onError(e: Throwable) {
  2518. Log.e(
  2519. TAG,
  2520. "Something went wrong when trying to delete message with id " +
  2521. message?.id,
  2522. e
  2523. )
  2524. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  2525. }
  2526. override fun onComplete() {
  2527. // unused atm
  2528. }
  2529. })
  2530. }
  2531. }
  2532. fun replyPrivately(message: IMessage?) {
  2533. val apiVersion =
  2534. ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  2535. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  2536. apiVersion,
  2537. conversationUser?.baseUrl,
  2538. "1",
  2539. null,
  2540. message?.user?.id?.substring(INVITE_LENGTH),
  2541. null
  2542. )
  2543. ncApi.createRoom(
  2544. credentials,
  2545. retrofitBucket.url,
  2546. retrofitBucket.queryMap
  2547. )
  2548. .subscribeOn(Schedulers.io())
  2549. .observeOn(AndroidSchedulers.mainThread())
  2550. .subscribe(object : Observer<RoomOverall> {
  2551. override fun onSubscribe(d: Disposable) {
  2552. // unused atm
  2553. }
  2554. override fun onNext(roomOverall: RoomOverall) {
  2555. val bundle = Bundle()
  2556. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  2557. bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
  2558. bundle.putString(KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
  2559. // FIXME once APIv2+ is used only, the createRoom already returns all the data
  2560. ncApi.getRoom(
  2561. credentials,
  2562. ApiUtils.getUrlForRoom(
  2563. apiVersion,
  2564. conversationUser?.baseUrl,
  2565. roomOverall.ocs!!.data!!.token
  2566. )
  2567. )
  2568. .subscribeOn(Schedulers.io())
  2569. .observeOn(AndroidSchedulers.mainThread())
  2570. .subscribe(object : Observer<RoomOverall> {
  2571. override fun onSubscribe(d: Disposable) {
  2572. // unused atm
  2573. }
  2574. override fun onNext(roomOverall: RoomOverall) {
  2575. bundle.putParcelable(
  2576. KEY_ACTIVE_CONVERSATION,
  2577. Parcels.wrap(roomOverall.ocs!!.data!!)
  2578. )
  2579. remapChatController(
  2580. router,
  2581. conversationUser!!.id!!,
  2582. roomOverall.ocs!!.data!!.token!!,
  2583. bundle,
  2584. true
  2585. )
  2586. }
  2587. override fun onError(e: Throwable) {
  2588. Log.e(TAG, e.message, e)
  2589. }
  2590. override fun onComplete() {
  2591. // unused atm
  2592. }
  2593. })
  2594. }
  2595. override fun onError(e: Throwable) {
  2596. Log.e(TAG, e.message, e)
  2597. }
  2598. override fun onComplete() {
  2599. // unused atm
  2600. }
  2601. })
  2602. }
  2603. fun forwardMessage(message: IMessage?) {
  2604. val bundle = Bundle()
  2605. bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
  2606. bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text)
  2607. bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomId)
  2608. router.pushController(
  2609. RouterTransaction.with(ConversationsListController(bundle))
  2610. .pushChangeHandler(HorizontalChangeHandler())
  2611. .popChangeHandler(HorizontalChangeHandler())
  2612. )
  2613. }
  2614. fun markAsUnread(message: IMessage?) {
  2615. val chatMessage = message as ChatMessage?
  2616. if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2617. ncApi.setChatReadMarker(
  2618. credentials,
  2619. ApiUtils.getUrlForSetChatReadMarker(
  2620. ApiUtils.getChatApiVersion(conversationUser, intArrayOf(ApiUtils.APIv1)),
  2621. conversationUser?.baseUrl,
  2622. roomToken
  2623. ),
  2624. chatMessage.previousMessageId
  2625. )
  2626. .subscribeOn(Schedulers.io())
  2627. .observeOn(AndroidSchedulers.mainThread())
  2628. .subscribe(object : Observer<GenericOverall> {
  2629. override fun onSubscribe(d: Disposable) {
  2630. // unused atm
  2631. }
  2632. override fun onNext(t: GenericOverall) {
  2633. // unused atm
  2634. }
  2635. override fun onError(e: Throwable) {
  2636. Log.e(TAG, e.message, e)
  2637. }
  2638. override fun onComplete() {
  2639. // unused atm
  2640. }
  2641. })
  2642. }
  2643. }
  2644. fun copyMessage(message: IMessage?) {
  2645. val clipboardManager =
  2646. activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
  2647. val clipData = ClipData.newPlainText(
  2648. resources?.getString(R.string.nc_app_product_name),
  2649. message?.text
  2650. )
  2651. clipboardManager.setPrimaryClip(clipData)
  2652. }
  2653. private fun hasVisibleItems(message: ChatMessage): Boolean {
  2654. return !message.isDeleted || // copy message
  2655. message.replyable || // reply to
  2656. message.replyable && // reply privately
  2657. conversationUser?.userId?.isNotEmpty() == true && conversationUser.userId != "?" &&
  2658. message.user.id.startsWith("users/") &&
  2659. message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
  2660. currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  2661. isShowMessageDeletionButton(message) || // delete
  2662. ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() || // forward
  2663. message.previousMessageId > NO_PREVIOUS_MESSAGE_ID && // mark as unread
  2664. ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() &&
  2665. BuildConfig.DEBUG
  2666. }
  2667. fun replyToMessage(message: IMessage?) {
  2668. val chatMessage = message as ChatMessage?
  2669. chatMessage?.let {
  2670. binding.messageInputView.findViewById<ImageButton>(R.id.attachmentButton)?.visibility =
  2671. View.GONE
  2672. binding.messageInputView.findViewById<ImageButton>(R.id.cancelReplyButton)?.visibility =
  2673. View.VISIBLE
  2674. val quotedMessage = binding
  2675. .messageInputView
  2676. .findViewById<EmojiTextView>(R.id.quotedMessage)
  2677. quotedMessage?.maxLines = 2
  2678. quotedMessage?.ellipsize = TextUtils.TruncateAt.END
  2679. quotedMessage?.text = it.text
  2680. binding.messageInputView.findViewById<EmojiTextView>(R.id.quotedMessageAuthor)?.text =
  2681. it.actorDisplayName ?: context.getText(R.string.nc_nick_guest)
  2682. conversationUser?.let { currentUser ->
  2683. val quotedMessageImage = binding
  2684. .messageInputView
  2685. .findViewById<ImageView>(R.id.quotedMessageImage)
  2686. chatMessage.imageUrl?.let { previewImageUrl ->
  2687. quotedMessageImage?.visibility = View.VISIBLE
  2688. val px = TypedValue.applyDimension(
  2689. TypedValue.COMPLEX_UNIT_DIP,
  2690. QUOTED_MESSAGE_IMAGE_MAX_HEIGHT,
  2691. resources?.displayMetrics
  2692. )
  2693. quotedMessageImage?.maxHeight = px.toInt()
  2694. val layoutParams = quotedMessageImage?.layoutParams as FlexboxLayout.LayoutParams
  2695. layoutParams.flexGrow = 0f
  2696. quotedMessageImage.layoutParams = layoutParams
  2697. quotedMessageImage.load(previewImageUrl) {
  2698. addHeader("Authorization", credentials!!)
  2699. }
  2700. } ?: run {
  2701. binding
  2702. .messageInputView
  2703. .findViewById<ImageView>(R.id.quotedMessageImage)
  2704. ?.visibility = View.GONE
  2705. }
  2706. }
  2707. val quotedChatMessageView = binding
  2708. .messageInputView
  2709. .findViewById<RelativeLayout>(R.id.quotedChatMessageView)
  2710. quotedChatMessageView?.tag = message?.jsonMessageId
  2711. quotedChatMessageView?.visibility = View.VISIBLE
  2712. }
  2713. }
  2714. private fun showMicrophoneButton(show: Boolean) {
  2715. if (show && CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "voice-message-sharing")) {
  2716. binding.messageInputView.messageSendButton.visibility = View.GONE
  2717. binding.messageInputView.recordAudioButton.visibility = View.VISIBLE
  2718. } else {
  2719. binding.messageInputView.messageSendButton.visibility = View.VISIBLE
  2720. binding.messageInputView.recordAudioButton.visibility = View.GONE
  2721. }
  2722. }
  2723. private fun setMessageAsDeleted(message: IMessage?) {
  2724. val messageTemp = message as ChatMessage
  2725. messageTemp.isDeleted = true
  2726. messageTemp.isOneToOneConversation =
  2727. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2728. messageTemp.activeUser = conversationUser
  2729. adapter?.update(messageTemp)
  2730. }
  2731. private fun updateAdapterForReaction(message: IMessage?) {
  2732. val messageTemp = message as ChatMessage
  2733. messageTemp.isOneToOneConversation =
  2734. currentConversation?.type == Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2735. messageTemp.activeUser = conversationUser
  2736. adapter?.update(messageTemp)
  2737. }
  2738. fun updateAdapterAfterSendReaction(message: ChatMessage, emoji: String) {
  2739. if (message.reactions == null) {
  2740. message.reactions = LinkedHashMap()
  2741. }
  2742. if (message.reactionsSelf == null) {
  2743. message.reactionsSelf = ArrayList<String>()
  2744. }
  2745. var amount = message.reactions!![emoji]
  2746. if (amount == null) {
  2747. amount = 0
  2748. }
  2749. message.reactions!![emoji] = amount + 1
  2750. message.reactionsSelf!!.add(emoji)
  2751. adapter?.update(message)
  2752. }
  2753. private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
  2754. if (conversationUser == null) return false
  2755. val isUserAllowedByPrivileges = if (message.actorId == conversationUser.userId) {
  2756. true
  2757. } else {
  2758. currentConversation!!.canModerate(conversationUser)
  2759. }
  2760. val isOlderThanSixHours = message
  2761. .createdAt
  2762. .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
  2763. return when {
  2764. !isUserAllowedByPrivileges -> false
  2765. isOlderThanSixHours -> false
  2766. message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
  2767. message.isDeleted -> false
  2768. message.hasFileAttachment() -> false
  2769. OBJECT_MESSAGE == message.message -> false
  2770. !CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "delete-messages") -> false
  2771. !participantPermissions.hasChatPermission() -> false
  2772. else -> true
  2773. }
  2774. }
  2775. override fun hasContentFor(message: ChatMessage, type: Byte): Boolean {
  2776. return when (type) {
  2777. CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
  2778. CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
  2779. CONTENT_TYPE_POLL -> message.isPoll()
  2780. CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview()
  2781. CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
  2782. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == "-1"
  2783. else -> false
  2784. }
  2785. }
  2786. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  2787. fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
  2788. /*
  2789. switch (webSocketCommunicationEvent.getType()) {
  2790. case "refreshChat":
  2791. if (
  2792. webSocketCommunicationEvent
  2793. .getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID)
  2794. .equals(Long.toString(conversationUser.getId()))
  2795. ) {
  2796. if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) {
  2797. pullChatMessages(2);
  2798. }
  2799. }
  2800. break;
  2801. default:
  2802. }*/
  2803. }
  2804. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  2805. fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) {
  2806. if (currentConversation?.type != Conversation.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  2807. currentConversation?.name != userMentionClickEvent.userId
  2808. ) {
  2809. var apiVersion = 1
  2810. // FIXME Fix API checking with guests?
  2811. if (conversationUser != null) {
  2812. apiVersion = ApiUtils.getConversationApiVersion(conversationUser, intArrayOf(ApiUtils.APIv4, 1))
  2813. }
  2814. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  2815. apiVersion,
  2816. conversationUser?.baseUrl,
  2817. "1",
  2818. null,
  2819. userMentionClickEvent.userId,
  2820. null
  2821. )
  2822. ncApi.createRoom(
  2823. credentials,
  2824. retrofitBucket.url,
  2825. retrofitBucket.queryMap
  2826. )
  2827. ?.subscribeOn(Schedulers.io())
  2828. ?.observeOn(AndroidSchedulers.mainThread())
  2829. ?.subscribe(object : Observer<RoomOverall> {
  2830. override fun onSubscribe(d: Disposable) {
  2831. // unused atm
  2832. }
  2833. override fun onNext(roomOverall: RoomOverall) {
  2834. val conversationIntent = Intent(activity, CallActivity::class.java)
  2835. val bundle = Bundle()
  2836. bundle.putParcelable(KEY_USER_ENTITY, conversationUser)
  2837. bundle.putString(KEY_ROOM_TOKEN, roomOverall.ocs!!.data!!.token)
  2838. bundle.putString(KEY_ROOM_ID, roomOverall.ocs!!.data!!.roomId)
  2839. if (conversationUser != null) {
  2840. bundle.putParcelable(
  2841. KEY_ACTIVE_CONVERSATION,
  2842. Parcels.wrap(roomOverall.ocs!!.data)
  2843. )
  2844. conversationIntent.putExtras(bundle)
  2845. ConductorRemapping.remapChatController(
  2846. router,
  2847. conversationUser.id!!,
  2848. roomOverall.ocs!!.data!!.token!!,
  2849. bundle,
  2850. false
  2851. )
  2852. } else {
  2853. conversationIntent.putExtras(bundle)
  2854. startActivity(conversationIntent)
  2855. Handler().postDelayed(
  2856. {
  2857. if (!isDestroyed && !isBeingDestroyed) {
  2858. router.popCurrentController()
  2859. }
  2860. },
  2861. POP_CURRENT_CONTROLLER_DELAY
  2862. )
  2863. }
  2864. }
  2865. override fun onError(e: Throwable) {
  2866. // unused atm
  2867. }
  2868. override fun onComplete() {
  2869. // unused atm
  2870. }
  2871. })
  2872. }
  2873. }
  2874. fun sendPictureFromCamIntent() {
  2875. if (!permissionUtil.isCameraPermissionGranted()) {
  2876. requestCameraPermissions()
  2877. } else {
  2878. startActivityForResult(TakePhotoActivity.createIntent(context), REQUEST_CODE_PICK_CAMERA)
  2879. }
  2880. }
  2881. fun sendVideoFromCamIntent() {
  2882. if (!permissionUtil.isCameraPermissionGranted()) {
  2883. requestCameraPermissions()
  2884. } else {
  2885. Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
  2886. takeVideoIntent.resolveActivity(activity!!.packageManager)?.also {
  2887. val videoFile: File? = try {
  2888. val outputDir = context.cacheDir
  2889. val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT)
  2890. val date = dateFormat.format(Date())
  2891. val videoName = String.format(
  2892. context.resources.getString(R.string.nc_video_filename),
  2893. date
  2894. )
  2895. File("$outputDir/$videoName$VIDEO_SUFFIX")
  2896. } catch (e: IOException) {
  2897. Toast.makeText(context, R.string.nc_common_error_sorry, Toast.LENGTH_LONG).show()
  2898. Log.e(TAG, "error while creating video file", e)
  2899. null
  2900. }
  2901. videoFile?.also {
  2902. videoURI = FileProvider.getUriForFile(context, context.packageName, it)
  2903. takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI)
  2904. startActivityForResult(takeVideoIntent, REQUEST_CODE_PICK_CAMERA)
  2905. }
  2906. }
  2907. }
  2908. }
  2909. }
  2910. fun createPoll() {
  2911. val pollVoteDialog = PollCreateDialogFragment.newInstance(
  2912. roomToken!!
  2913. )
  2914. pollVoteDialog.show(
  2915. (activity as MainActivity?)!!.supportFragmentManager,
  2916. TAG
  2917. )
  2918. }
  2919. companion object {
  2920. private const val TAG = "ChatController"
  2921. private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
  2922. private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 2
  2923. private const val CONTENT_TYPE_LOCATION: Byte = 3
  2924. private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 4
  2925. private const val CONTENT_TYPE_POLL: Byte = 5
  2926. private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 6
  2927. private const val NEW_MESSAGES_POPUP_BUBBLE_DELAY: Long = 200
  2928. private const val POP_CURRENT_CONTROLLER_DELAY: Long = 100
  2929. private const val LOBBY_TIMER_DELAY: Long = 5000
  2930. private const val HTTP_CODE_OK: Int = 200
  2931. private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
  2932. private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
  2933. private const val REQUEST_CODE_SELECT_CONTACT: Int = 666
  2934. private const val REQUEST_CODE_MESSAGE_SEARCH: Int = 777
  2935. private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
  2936. private const val REQUEST_READ_CONTACT_PERMISSION = 234
  2937. private const val REQUEST_CAMERA_PERMISSION = 223
  2938. private const val REQUEST_CODE_PICK_CAMERA: Int = 333
  2939. private const val REQUEST_CODE_SELECT_REMOTE_FILES = 888
  2940. private const val OBJECT_MESSAGE: String = "{object}"
  2941. private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
  2942. private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
  2943. private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
  2944. private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
  2945. private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
  2946. private const val VIDEO_SUFFIX = ".mp4"
  2947. private const val SHORT_VIBRATE: Long = 20
  2948. private const val FULLY_OPAQUE_INT: Int = 255
  2949. private const val SEMI_TRANSPARENT_INT: Int = 99
  2950. private const val VOICE_MESSAGE_SEEKBAR_BASE: Int = 1000
  2951. private const val SECOND: Long = 1000
  2952. private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
  2953. private const val GROUPED_MESSAGES_THRESHOLD = 4
  2954. private const val GROUPED_MESSAGES_SAME_AUTHOR_THRESHOLD = 5
  2955. private const val TOOLBAR_AVATAR_RATIO = 1.5
  2956. private const val HTTP_CODE_NOT_MODIFIED = 304
  2957. private const val HTTP_CODE_PRECONDITION_FAILED = 412
  2958. private const val QUOTED_MESSAGE_IMAGE_MAX_HEIGHT = 96f
  2959. private const val MENTION_AUTO_COMPLETE_ELEVATION = 6f
  2960. private const val MESSAGE_PULL_LIMIT = 100
  2961. private const val INVITE_LENGTH = 6
  2962. private const val ACTOR_LENGTH = 6
  2963. private const val ANIMATION_DURATION: Long = 750
  2964. private const val RETRIES: Long = 3
  2965. private const val LOOKING_INTO_FUTURE_TIMEOUT = 30
  2966. private const val CHUNK_SIZE: Int = 10
  2967. private const val ONE_SECOND_IN_MILLIS = 1000
  2968. }
  2969. }