ChatActivity.kt 150 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787
  1. /*
  2. * Nextcloud Talk - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
  5. * SPDX-FileCopyrightText: 2024 Giacomo Pacini <giacomo@paciosoft.com>
  6. * SPDX-FileCopyrightText: 2023 Ezhil Shanmugham <ezhil56x.contact@gmail.com>
  7. * SPDX-FileCopyrightText: 2021-2022 Marcel Hibbe <dev@mhibbe.de>
  8. * SPDX-FileCopyrightText: 2021-2022 Tim Krüger <t@timkrueger.me>
  9. * SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
  10. * SPDX-FileCopyrightText: 2017-2019 Mario Danic <mario@lovelyhq.com>
  11. * SPDX-License-Identifier: GPL-3.0-or-later
  12. */
  13. package com.nextcloud.talk.chat
  14. import android.Manifest
  15. import android.annotation.SuppressLint
  16. import android.app.Activity
  17. import android.content.ClipData
  18. import android.content.ClipboardManager
  19. import android.content.Context
  20. import android.content.Intent
  21. import android.content.pm.PackageManager
  22. import android.content.res.AssetFileDescriptor
  23. import android.database.Cursor
  24. import android.graphics.drawable.BitmapDrawable
  25. import android.graphics.drawable.ColorDrawable
  26. import android.graphics.drawable.Drawable
  27. import android.media.MediaPlayer
  28. import android.net.Uri
  29. import android.os.Build
  30. import android.os.Bundle
  31. import android.os.Handler
  32. import android.provider.ContactsContract
  33. import android.provider.MediaStore
  34. import android.text.SpannableStringBuilder
  35. import android.text.TextUtils
  36. import android.util.Log
  37. import android.view.Gravity
  38. import android.view.Menu
  39. import android.view.MenuItem
  40. import android.view.View
  41. import android.view.animation.AccelerateDecelerateInterpolator
  42. import android.widget.AbsListView
  43. import android.widget.FrameLayout
  44. import android.widget.ImageView
  45. import android.widget.PopupMenu
  46. import android.widget.TextView
  47. import androidx.activity.OnBackPressedCallback
  48. import androidx.activity.result.ActivityResult
  49. import androidx.activity.result.contract.ActivityResultContracts
  50. import androidx.appcompat.view.ContextThemeWrapper
  51. import androidx.core.content.FileProvider
  52. import androidx.core.content.PermissionChecker
  53. import androidx.core.content.PermissionChecker.PERMISSION_GRANTED
  54. import androidx.core.graphics.drawable.toBitmap
  55. import androidx.core.text.bold
  56. import androidx.emoji2.text.EmojiCompat
  57. import androidx.fragment.app.DialogFragment
  58. import androidx.fragment.app.commit
  59. import androidx.lifecycle.ViewModelProvider
  60. import androidx.lifecycle.lifecycleScope
  61. import androidx.recyclerview.widget.ItemTouchHelper
  62. import androidx.recyclerview.widget.LinearLayoutManager
  63. import androidx.recyclerview.widget.RecyclerView
  64. import androidx.work.Data
  65. import androidx.work.OneTimeWorkRequest
  66. import androidx.work.WorkInfo
  67. import androidx.work.WorkManager
  68. import autodagger.AutoInjector
  69. import coil.imageLoader
  70. import coil.request.CachePolicy
  71. import coil.request.ImageRequest
  72. import coil.target.Target
  73. import coil.transform.CircleCropTransformation
  74. import com.google.android.material.snackbar.Snackbar
  75. import com.nextcloud.android.common.ui.theme.utils.ColorRole
  76. import com.nextcloud.talk.BuildConfig
  77. import com.nextcloud.talk.R
  78. import com.nextcloud.talk.activities.BaseActivity
  79. import com.nextcloud.talk.activities.CallActivity
  80. import com.nextcloud.talk.activities.TakePhotoActivity
  81. import com.nextcloud.talk.adapters.messages.CallStartedMessageInterface
  82. import com.nextcloud.talk.adapters.messages.CallStartedViewHolder
  83. import com.nextcloud.talk.adapters.messages.CommonMessageInterface
  84. import com.nextcloud.talk.adapters.messages.IncomingDeckCardViewHolder
  85. import com.nextcloud.talk.adapters.messages.IncomingLinkPreviewMessageViewHolder
  86. import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
  87. import com.nextcloud.talk.adapters.messages.IncomingPollMessageViewHolder
  88. import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
  89. import com.nextcloud.talk.adapters.messages.IncomingTextMessageViewHolder
  90. import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
  91. import com.nextcloud.talk.adapters.messages.MessagePayload
  92. import com.nextcloud.talk.adapters.messages.OutcomingDeckCardViewHolder
  93. import com.nextcloud.talk.adapters.messages.OutcomingLinkPreviewMessageViewHolder
  94. import com.nextcloud.talk.adapters.messages.OutcomingLocationMessageViewHolder
  95. import com.nextcloud.talk.adapters.messages.OutcomingPollMessageViewHolder
  96. import com.nextcloud.talk.adapters.messages.OutcomingPreviewMessageViewHolder
  97. import com.nextcloud.talk.adapters.messages.OutcomingTextMessageViewHolder
  98. import com.nextcloud.talk.adapters.messages.OutcomingVoiceMessageViewHolder
  99. import com.nextcloud.talk.adapters.messages.PreviewMessageInterface
  100. import com.nextcloud.talk.adapters.messages.PreviewMessageViewHolder
  101. import com.nextcloud.talk.adapters.messages.SystemMessageInterface
  102. import com.nextcloud.talk.adapters.messages.SystemMessageViewHolder
  103. import com.nextcloud.talk.adapters.messages.TalkMessagesListAdapter
  104. import com.nextcloud.talk.adapters.messages.TemporaryMessageInterface
  105. import com.nextcloud.talk.adapters.messages.TemporaryMessageViewHolder
  106. import com.nextcloud.talk.adapters.messages.UnreadNoticeMessageViewHolder
  107. import com.nextcloud.talk.adapters.messages.VoiceMessageInterface
  108. import com.nextcloud.talk.api.NcApi
  109. import com.nextcloud.talk.application.NextcloudTalkApplication
  110. import com.nextcloud.talk.chat.data.model.ChatMessage
  111. import com.nextcloud.talk.chat.viewmodels.ChatViewModel
  112. import com.nextcloud.talk.chat.viewmodels.MessageInputViewModel
  113. import com.nextcloud.talk.conversationinfo.ConversationInfoActivity
  114. import com.nextcloud.talk.conversationlist.ConversationsListActivity
  115. import com.nextcloud.talk.data.user.model.User
  116. import com.nextcloud.talk.databinding.ActivityChatBinding
  117. import com.nextcloud.talk.events.UserMentionClickEvent
  118. import com.nextcloud.talk.events.WebSocketCommunicationEvent
  119. import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
  120. import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
  121. import com.nextcloud.talk.jobs.ShareOperationWorker
  122. import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
  123. import com.nextcloud.talk.location.LocationPickerActivity
  124. import com.nextcloud.talk.messagesearch.MessageSearchActivity
  125. import com.nextcloud.talk.models.ExternalSignalingServer
  126. import com.nextcloud.talk.models.domain.ConversationModel
  127. import com.nextcloud.talk.models.json.capabilities.SpreedCapability
  128. import com.nextcloud.talk.models.json.chat.ReadStatus
  129. import com.nextcloud.talk.models.json.conversations.ConversationEnums
  130. import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall
  131. import com.nextcloud.talk.polls.ui.PollCreateDialogFragment
  132. import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
  133. import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
  134. import com.nextcloud.talk.signaling.SignalingMessageReceiver
  135. import com.nextcloud.talk.signaling.SignalingMessageSender
  136. import com.nextcloud.talk.translate.ui.TranslateActivity
  137. import com.nextcloud.talk.ui.StatusDrawable
  138. import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
  139. import com.nextcloud.talk.ui.dialog.DateTimePickerFragment
  140. import com.nextcloud.talk.ui.dialog.FileAttachmentPreviewFragment
  141. import com.nextcloud.talk.ui.dialog.MessageActionsDialog
  142. import com.nextcloud.talk.ui.dialog.SaveToStorageDialogFragment
  143. import com.nextcloud.talk.ui.dialog.ShowReactionsDialog
  144. import com.nextcloud.talk.ui.recyclerview.MessageSwipeActions
  145. import com.nextcloud.talk.ui.recyclerview.MessageSwipeCallback
  146. import com.nextcloud.talk.utils.ApiUtils
  147. import com.nextcloud.talk.utils.AudioUtils
  148. import com.nextcloud.talk.utils.CapabilitiesUtil
  149. import com.nextcloud.talk.utils.ContactUtils
  150. import com.nextcloud.talk.utils.ConversationUtils
  151. import com.nextcloud.talk.utils.DateConstants
  152. import com.nextcloud.talk.utils.DateUtils
  153. import com.nextcloud.talk.utils.DisplayUtils
  154. import com.nextcloud.talk.utils.FileUtils
  155. import com.nextcloud.talk.utils.FileViewerUtils
  156. import com.nextcloud.talk.utils.Mimetype
  157. import com.nextcloud.talk.utils.NotificationUtils
  158. import com.nextcloud.talk.utils.ParticipantPermissions
  159. import com.nextcloud.talk.utils.SpreedFeatures
  160. import com.nextcloud.talk.utils.VibrationUtils
  161. import com.nextcloud.talk.utils.bundle.BundleKeys
  162. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY
  163. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
  164. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
  165. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
  166. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_BREAKOUT_ROOM
  167. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_IS_MODERATOR
  168. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_RECORDING_STATE
  169. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
  170. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_START_CALL_AFTER_ROOM_SWITCH
  171. import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SWITCH_TO_ROOM
  172. import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
  173. import com.nextcloud.talk.utils.rx.DisposableSet
  174. import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder
  175. import com.nextcloud.talk.webrtc.WebSocketConnectionHelper
  176. import com.nextcloud.talk.webrtc.WebSocketInstance
  177. import com.otaliastudios.autocomplete.Autocomplete
  178. import com.stfalcon.chatkit.commons.ImageLoader
  179. import com.stfalcon.chatkit.commons.models.IMessage
  180. import com.stfalcon.chatkit.messages.MessageHolders
  181. import com.stfalcon.chatkit.messages.MessageHolders.ContentChecker
  182. import com.stfalcon.chatkit.messages.MessagesListAdapter
  183. import com.stfalcon.chatkit.utils.DateFormatter
  184. import io.reactivex.Observer
  185. import io.reactivex.disposables.Disposable
  186. import kotlinx.coroutines.CoroutineScope
  187. import kotlinx.coroutines.Dispatchers
  188. import kotlinx.coroutines.delay
  189. import kotlinx.coroutines.flow.collect
  190. import kotlinx.coroutines.flow.onEach
  191. import kotlinx.coroutines.launch
  192. import kotlinx.coroutines.withContext
  193. import org.greenrobot.eventbus.Subscribe
  194. import org.greenrobot.eventbus.ThreadMode
  195. import retrofit2.HttpException
  196. import java.io.File
  197. import java.io.IOException
  198. import java.net.HttpURLConnection
  199. import java.text.SimpleDateFormat
  200. import java.util.Date
  201. import java.util.Locale
  202. import java.util.concurrent.ExecutionException
  203. import javax.inject.Inject
  204. import kotlin.collections.set
  205. import kotlin.math.roundToInt
  206. @AutoInjector(NextcloudTalkApplication::class)
  207. class ChatActivity :
  208. BaseActivity(),
  209. MessagesListAdapter.OnLoadMoreListener,
  210. MessagesListAdapter.Formatter<Date>,
  211. MessagesListAdapter.OnMessageViewLongClickListener<IMessage>,
  212. ContentChecker<ChatMessage>,
  213. VoiceMessageInterface,
  214. CommonMessageInterface,
  215. PreviewMessageInterface,
  216. SystemMessageInterface,
  217. CallStartedMessageInterface,
  218. TemporaryMessageInterface {
  219. var active = false
  220. private lateinit var binding: ActivityChatBinding
  221. @Inject
  222. lateinit var ncApi: NcApi
  223. @Inject
  224. lateinit var permissionUtil: PlatformPermissionUtil
  225. @Inject
  226. lateinit var dateUtils: DateUtils
  227. @Inject
  228. lateinit var viewModelFactory: ViewModelProvider.Factory
  229. lateinit var chatViewModel: ChatViewModel
  230. lateinit var messageInputViewModel: MessageInputViewModel
  231. private val startSelectContactForResult = registerForActivityResult(
  232. ActivityResultContracts
  233. .StartActivityForResult()
  234. ) {
  235. executeIfResultOk(it) { intent ->
  236. onSelectContactResult(intent)
  237. }
  238. }
  239. private val startChooseFileIntentForResult = registerForActivityResult(
  240. ActivityResultContracts.StartActivityForResult()
  241. ) {
  242. executeIfResultOk(it) { intent ->
  243. onChooseFileResult(intent)
  244. }
  245. }
  246. private val startRemoteFileBrowsingForResult = registerForActivityResult(
  247. ActivityResultContracts.StartActivityForResult()
  248. ) {
  249. executeIfResultOk(it) { intent ->
  250. onRemoteFileBrowsingResult(intent)
  251. }
  252. }
  253. private val startMessageSearchForResult =
  254. registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
  255. executeIfResultOk(it) { intent ->
  256. onMessageSearchResult(intent)
  257. }
  258. }
  259. private val startPickCameraIntentForResult = registerForActivityResult(
  260. ActivityResultContracts.StartActivityForResult()
  261. ) {
  262. executeIfResultOk(it) { intent ->
  263. onPickCameraResult(intent)
  264. }
  265. }
  266. override val view: View
  267. get() = binding.root
  268. val disposables = DisposableSet()
  269. var sessionIdAfterRoomJoined: String? = null
  270. lateinit var roomToken: String
  271. var conversationUser: User? = null
  272. lateinit var spreedCapabilities: SpreedCapability
  273. var chatApiVersion: Int = 1
  274. private var roomPassword: String = ""
  275. var credentials: String? = null
  276. var currentConversation: ConversationModel? = null
  277. var adapter: TalkMessagesListAdapter<ChatMessage>? = null
  278. var mentionAutocomplete: Autocomplete<*>? = null
  279. var layoutManager: LinearLayoutManager? = null
  280. var pullChatMessagesPending = false
  281. var startCallFromNotification: Boolean = false
  282. var startCallFromRoomSwitch: Boolean = false
  283. // lateinit var roomId: String
  284. var voiceOnly: Boolean = true
  285. private lateinit var path: String
  286. var myFirstMessage: CharSequence? = null
  287. var checkingLobbyStatus: Boolean = false
  288. private var conversationVoiceCallMenuItem: MenuItem? = null
  289. private var conversationVideoMenuItem: MenuItem? = null
  290. var webSocketInstance: WebSocketInstance? = null
  291. var signalingMessageSender: SignalingMessageSender? = null
  292. var externalSignalingServer: ExternalSignalingServer? = null
  293. var getRoomInfoTimerHandler: Handler? = null
  294. private val filesToUpload: MutableList<String> = ArrayList()
  295. lateinit var sharedText: String
  296. var mediaPlayer: MediaPlayer? = null
  297. var mediaPlayerHandler: Handler? = null
  298. private var currentlyPlayedVoiceMessage: ChatMessage? = null
  299. // messy workaround for a mediaPlayer bug, don't delete
  300. private var lastRecordMediaPosition: Int = 0
  301. private var lastRecordedSeeked: Boolean = false
  302. lateinit var participantPermissions: ParticipantPermissions
  303. private var videoURI: Uri? = null
  304. private val onBackPressedCallback = object : OnBackPressedCallback(true) {
  305. override fun handleOnBackPressed() {
  306. chatViewModel.handleChatOnBackPress()
  307. if (currentlyPlayedVoiceMessage != null) {
  308. stopMediaPlayer(currentlyPlayedVoiceMessage!!)
  309. }
  310. val intent = Intent(this@ChatActivity, ConversationsListActivity::class.java)
  311. intent.putExtras(Bundle())
  312. startActivity(intent)
  313. }
  314. }
  315. private lateinit var messageInputFragment: MessageInputFragment
  316. val typingParticipants = HashMap<String, TypingParticipant>()
  317. var callStarted = false
  318. private var voiceMessageToRestoreId = ""
  319. private var voiceMessageToRestoreAudioPosition = 0
  320. private var voiceMessageToRestoreWasPlaying = false
  321. private val localParticipantMessageListener = object : SignalingMessageReceiver.LocalParticipantMessageListener {
  322. override fun onSwitchTo(token: String?) {
  323. if (token != null) {
  324. if (CallActivity.active) {
  325. Log.d(TAG, "CallActivity is running. Ignore to switch chat in ChatActivity...")
  326. } else {
  327. switchToRoom(token, false, false)
  328. }
  329. }
  330. }
  331. }
  332. private val conversationMessageListener = object : SignalingMessageReceiver.ConversationMessageListener {
  333. override fun onStartTyping(userId: String?, session: String?) {
  334. val userIdOrGuestSession = userId ?: session
  335. if (isTypingStatusEnabled() && conversationUser?.userId != userIdOrGuestSession) {
  336. var displayName = webSocketInstance?.getDisplayNameForSession(session)
  337. if (displayName != null && !typingParticipants.contains(userIdOrGuestSession)) {
  338. if (displayName == "") {
  339. displayName = context.resources?.getString(R.string.nc_guest)!!
  340. }
  341. runOnUiThread {
  342. val typingParticipant = TypingParticipant(userIdOrGuestSession!!, displayName) {
  343. typingParticipants.remove(userIdOrGuestSession)
  344. updateTypingIndicator()
  345. }
  346. typingParticipants[userIdOrGuestSession] = typingParticipant
  347. updateTypingIndicator()
  348. }
  349. } else if (typingParticipants.contains(userIdOrGuestSession)) {
  350. typingParticipants[userIdOrGuestSession]?.restartTimer()
  351. }
  352. }
  353. }
  354. override fun onStopTyping(userId: String?, session: String?) {
  355. val userIdOrGuestSession = userId ?: session
  356. if (isTypingStatusEnabled() && conversationUser?.userId != userId) {
  357. typingParticipants[userIdOrGuestSession]?.cancelTimer()
  358. typingParticipants.remove(userIdOrGuestSession)
  359. updateTypingIndicator()
  360. }
  361. }
  362. }
  363. override fun onCreate(savedInstanceState: Bundle?) {
  364. super.onCreate(savedInstanceState)
  365. NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
  366. binding = ActivityChatBinding.inflate(layoutInflater)
  367. setupActionBar()
  368. setContentView(binding.root)
  369. setupSystemColors()
  370. messageInputFragment = MessageInputFragment()
  371. conversationUser = currentUserProvider.currentUser.blockingGet()
  372. handleIntent(intent)
  373. chatViewModel = ViewModelProvider(this, viewModelFactory)[ChatViewModel::class.java]
  374. messageInputViewModel = ViewModelProvider(this, viewModelFactory)[MessageInputViewModel::class.java]
  375. this.lifecycleScope.launch {
  376. delay(DELAY_TO_SHOW_PROGRESS_BAR)
  377. if (adapter?.isEmpty == true) {
  378. binding.progressBar.visibility = View.VISIBLE
  379. }
  380. }
  381. onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
  382. initObservers()
  383. if (savedInstanceState != null) {
  384. // Restore value of members from saved state
  385. var voiceMessageId = savedInstanceState.getString(CURRENT_AUDIO_MESSAGE_KEY, "")
  386. var voiceMessagePosition = savedInstanceState.getInt(CURRENT_AUDIO_POSITION_KEY, 0)
  387. var wasAudioPLaying = savedInstanceState.getBoolean(CURRENT_AUDIO_WAS_PLAYING_KEY, false)
  388. if (!voiceMessageId.equals("")) {
  389. Log.d(RESUME_AUDIO_TAG, "restored voice messageID: " + voiceMessageId)
  390. Log.d(RESUME_AUDIO_TAG, "audio position: " + voiceMessagePosition)
  391. Log.d(RESUME_AUDIO_TAG, "audio was playing: " + wasAudioPLaying.toString())
  392. voiceMessageToRestoreId = voiceMessageId
  393. voiceMessageToRestoreAudioPosition = voiceMessagePosition
  394. voiceMessageToRestoreWasPlaying = wasAudioPLaying
  395. } else {
  396. Log.d(RESUME_AUDIO_TAG, "stored voice message id is empty, not resuming audio playing")
  397. voiceMessageToRestoreId = ""
  398. voiceMessageToRestoreAudioPosition = 0
  399. voiceMessageToRestoreWasPlaying = false
  400. }
  401. } else {
  402. voiceMessageToRestoreId = ""
  403. voiceMessageToRestoreAudioPosition = 0
  404. voiceMessageToRestoreWasPlaying = false
  405. }
  406. }
  407. override fun onNewIntent(intent: Intent) {
  408. super.onNewIntent(intent)
  409. val extras: Bundle? = intent.extras
  410. val requestedRoomSwitch = extras?.getBoolean(KEY_SWITCH_TO_ROOM, false) == true
  411. if (requestedRoomSwitch) {
  412. val newRoomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty()
  413. val startCallAfterRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true
  414. val isVoiceOnlyCall = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true
  415. if (newRoomToken != roomToken) {
  416. switchToRoom(newRoomToken, startCallAfterRoomSwitch, isVoiceOnlyCall)
  417. }
  418. } else {
  419. handleIntent(intent)
  420. }
  421. }
  422. private fun handleIntent(intent: Intent) {
  423. val extras: Bundle? = intent.extras
  424. // roomId = extras?.getString(KEY_ROOM_ID).orEmpty()
  425. roomToken = extras?.getString(KEY_ROOM_TOKEN).orEmpty()
  426. sharedText = extras?.getString(BundleKeys.KEY_SHARED_TEXT).orEmpty()
  427. Log.d(TAG, " roomToken = $roomToken")
  428. if (roomToken.isEmpty()) {
  429. Log.d(TAG, " roomToken was null or empty!")
  430. }
  431. roomPassword = extras?.getString(BundleKeys.KEY_CONVERSATION_PASSWORD).orEmpty()
  432. credentials = if (conversationUser?.userId == "?") {
  433. null
  434. } else {
  435. ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
  436. }
  437. startCallFromNotification = extras?.getBoolean(BundleKeys.KEY_FROM_NOTIFICATION_START_CALL, false) == true
  438. startCallFromRoomSwitch = extras?.getBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, false) == true
  439. voiceOnly = extras?.getBoolean(KEY_CALL_VOICE_ONLY, false) == true
  440. }
  441. override fun onStart() {
  442. super.onStart()
  443. active = true
  444. this.lifecycle.addObserver(AudioUtils)
  445. this.lifecycle.addObserver(chatViewModel)
  446. }
  447. override fun onSaveInstanceState(outState: Bundle) {
  448. if (currentlyPlayedVoiceMessage != null) {
  449. outState.putString(CURRENT_AUDIO_MESSAGE_KEY, currentlyPlayedVoiceMessage!!.id)
  450. outState.putInt(CURRENT_AUDIO_POSITION_KEY, currentlyPlayedVoiceMessage!!.voiceMessagePlayedSeconds)
  451. outState.putBoolean(CURRENT_AUDIO_WAS_PLAYING_KEY, currentlyPlayedVoiceMessage!!.isPlayingVoiceMessage)
  452. Log.d(RESUME_AUDIO_TAG, "Stored current audio message ID: " + currentlyPlayedVoiceMessage!!.id)
  453. Log.d(
  454. RESUME_AUDIO_TAG,
  455. "Audio Position: " + currentlyPlayedVoiceMessage!!.voiceMessagePlayedSeconds
  456. .toString() + " | isPLaying: " + currentlyPlayedVoiceMessage!!.isPlayingVoiceMessage
  457. )
  458. }
  459. chatViewModel.handleOrientationChange()
  460. super.onSaveInstanceState(outState)
  461. }
  462. override fun onStop() {
  463. super.onStop()
  464. active = false
  465. this.lifecycle.removeObserver(AudioUtils)
  466. this.lifecycle.removeObserver(chatViewModel)
  467. }
  468. @SuppressLint("NotifyDataSetChanged")
  469. @Suppress("LongMethod")
  470. private fun initObservers() {
  471. Log.d(TAG, "initObservers Called")
  472. messageInputViewModel.messageQueueFlow.observe(this) { list ->
  473. list.forEachIndexed { _, qMsg ->
  474. val temporaryChatMessage = ChatMessage()
  475. temporaryChatMessage.jsonMessageId = -3
  476. temporaryChatMessage.actorId = "-3"
  477. temporaryChatMessage.timestamp = System.currentTimeMillis() / 1000
  478. temporaryChatMessage.message = qMsg.message.toString()
  479. temporaryChatMessage.tempMessageId = qMsg.id
  480. temporaryChatMessage.isTempMessage = true
  481. temporaryChatMessage.parentMessageId = qMsg.replyTo!!.toLong()
  482. val pos = adapter?.getMessagePositionById(qMsg.replyTo.toString())
  483. adapter?.addToStart(temporaryChatMessage, true)
  484. adapter?.notifyDataSetChanged()
  485. }
  486. }
  487. messageInputViewModel.messageQueueSizeFlow.observe(this) { size ->
  488. if (size == 0) {
  489. var i = 0
  490. var pos = adapter?.getMessagePositionById("-3")
  491. while (pos != null && pos > -1) {
  492. adapter?.items?.removeAt(pos)
  493. i++
  494. pos = adapter?.getMessagePositionById("-3")
  495. }
  496. adapter?.notifyDataSetChanged()
  497. }
  498. }
  499. this.lifecycleScope.launch {
  500. chatViewModel.getConversationFlow
  501. .onEach { conversationModel ->
  502. currentConversation = conversationModel
  503. val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
  504. val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
  505. chatViewModel.setData(
  506. currentConversation!!,
  507. credentials!!,
  508. urlForChatting
  509. )
  510. logConversationInfos("GetRoomSuccessState")
  511. if (adapter == null) {
  512. initAdapter()
  513. binding.messagesListView.setAdapter(adapter)
  514. layoutManager = binding.messagesListView.layoutManager as LinearLayoutManager?
  515. }
  516. chatViewModel.getCapabilities(conversationUser!!, roomToken, currentConversation!!)
  517. }.collect()
  518. }
  519. chatViewModel.getRoomViewState.observe(this) { state ->
  520. when (state) {
  521. is ChatViewModel.GetRoomSuccessState -> {
  522. // unused atm
  523. }
  524. is ChatViewModel.GetRoomErrorState -> {
  525. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  526. }
  527. else -> {}
  528. }
  529. }
  530. chatViewModel.getCapabilitiesViewState.observe(this) { state ->
  531. when (state) {
  532. is ChatViewModel.GetCapabilitiesUpdateState -> {
  533. if (currentConversation != null) {
  534. spreedCapabilities = state.spreedCapabilities
  535. chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  536. participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
  537. invalidateOptionsMenu()
  538. checkShowCallButtons()
  539. checkLobbyState()
  540. updateRoomTimerHandler()
  541. } else {
  542. Log.w(
  543. TAG,
  544. "currentConversation was null in observer ChatViewModel.GetCapabilitiesUpdateState"
  545. )
  546. }
  547. }
  548. is ChatViewModel.GetCapabilitiesInitialLoadState -> {
  549. if (currentConversation != null) {
  550. spreedCapabilities = state.spreedCapabilities
  551. chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  552. participantPermissions = ParticipantPermissions(spreedCapabilities, currentConversation!!)
  553. supportFragmentManager.commit {
  554. setReorderingAllowed(true) // optimizes out redundant replace operations
  555. replace(R.id.fragment_container_activity_chat, messageInputFragment)
  556. }
  557. joinRoomWithPassword()
  558. if (conversationUser?.userId != "?" &&
  559. CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MENTION_FLAG)
  560. ) {
  561. binding.chatToolbar.setOnClickListener { _ -> showConversationInfoScreen() }
  562. }
  563. loadAvatarForStatusBar()
  564. setupSwipeToReply()
  565. setActionBarTitle()
  566. checkShowCallButtons()
  567. checkLobbyState()
  568. updateRoomTimerHandler()
  569. val urlForChatting =
  570. ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
  571. if (adapter?.isEmpty == true) {
  572. chatViewModel.loadMessages(
  573. withCredentials = credentials!!,
  574. withUrl = urlForChatting
  575. )
  576. messageInputViewModel.getTempMessagesFromMessageQueue(currentConversation!!.internalId)
  577. }
  578. } else {
  579. Log.w(
  580. TAG,
  581. "currentConversation was null in observer ChatViewModel.GetCapabilitiesInitialLoadState"
  582. )
  583. }
  584. }
  585. is ChatViewModel.GetCapabilitiesErrorState -> {
  586. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  587. }
  588. else -> {}
  589. }
  590. }
  591. chatViewModel.joinRoomViewState.observe(this) { state ->
  592. when (state) {
  593. is ChatViewModel.JoinRoomSuccessState -> {
  594. currentConversation = state.conversationModel
  595. sessionIdAfterRoomJoined = currentConversation!!.sessionId
  596. ApplicationWideCurrentRoomHolder.getInstance().session = currentConversation!!.sessionId
  597. // ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = currentConversation!!.roomId
  598. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = currentConversation!!.token
  599. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  600. logConversationInfos("joinRoomWithPassword#onNext")
  601. setupWebsocket()
  602. if (webSocketInstance != null) {
  603. webSocketInstance?.joinRoomWithRoomTokenAndSession(
  604. roomToken,
  605. sessionIdAfterRoomJoined,
  606. externalSignalingServer?.federation
  607. )
  608. }
  609. if (startCallFromNotification != null && startCallFromNotification) {
  610. startCallFromNotification = false
  611. startACall(voiceOnly, false)
  612. }
  613. if (startCallFromRoomSwitch) {
  614. startCallFromRoomSwitch = false
  615. startACall(voiceOnly, true)
  616. }
  617. }
  618. is ChatViewModel.JoinRoomErrorState -> {}
  619. else -> {}
  620. }
  621. }
  622. chatViewModel.leaveRoomViewState.observe(this) { state ->
  623. when (state) {
  624. is ChatViewModel.LeaveRoomSuccessState -> {
  625. logConversationInfos("leaveRoom#onNext")
  626. checkingLobbyStatus = false
  627. if (getRoomInfoTimerHandler != null) {
  628. getRoomInfoTimerHandler?.removeCallbacksAndMessages(null)
  629. }
  630. if (webSocketInstance != null && currentConversation != null) {
  631. webSocketInstance?.joinRoomWithRoomTokenAndSession(
  632. "",
  633. sessionIdAfterRoomJoined
  634. )
  635. }
  636. sessionIdAfterRoomJoined = "0"
  637. if (state.funToCallWhenLeaveSuccessful != null) {
  638. Log.d(TAG, "a callback action was set and is now executed because room was left successfully")
  639. state.funToCallWhenLeaveSuccessful.invoke()
  640. }
  641. }
  642. else -> {}
  643. }
  644. }
  645. messageInputViewModel.sendChatMessageViewState.observe(this) { state ->
  646. when (state) {
  647. is MessageInputViewModel.SendChatMessageSuccessState -> {
  648. myFirstMessage = state.message
  649. removeUnreadMessagesMarker()
  650. if (binding.unreadMessagesPopup.isShown) {
  651. binding.unreadMessagesPopup.visibility = View.GONE
  652. }
  653. binding.messagesListView.smoothScrollToPosition(0)
  654. }
  655. is MessageInputViewModel.SendChatMessageErrorState -> {
  656. if (state.e is HttpException) {
  657. val code = state.e.code()
  658. if (code.toString().startsWith("2")) {
  659. myFirstMessage = state.message
  660. if (binding.unreadMessagesPopup.isShown) {
  661. binding.unreadMessagesPopup.visibility = View.GONE
  662. }
  663. binding.messagesListView.smoothScrollToPosition(0)
  664. }
  665. }
  666. }
  667. else -> {}
  668. }
  669. }
  670. chatViewModel.deleteChatMessageViewState.observe(this) { state ->
  671. when (state) {
  672. is ChatViewModel.DeleteChatMessageSuccessState -> {
  673. if (state.msg.ocs!!.meta!!.statusCode == HttpURLConnection.HTTP_ACCEPTED) {
  674. Snackbar.make(
  675. binding.root,
  676. R.string.nc_delete_message_leaked_to_matterbridge,
  677. Snackbar.LENGTH_LONG
  678. ).show()
  679. }
  680. val id = state.msg.ocs!!.data!!.parentMessage!!.id.toString()
  681. val index = adapter?.getMessagePositionById(id) ?: 0
  682. val message = adapter?.items?.get(index)?.item as ChatMessage
  683. setMessageAsDeleted(message)
  684. }
  685. is ChatViewModel.DeleteChatMessageErrorState -> {
  686. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  687. }
  688. else -> {}
  689. }
  690. }
  691. chatViewModel.createRoomViewState.observe(this) { state ->
  692. when (state) {
  693. is ChatViewModel.CreateRoomSuccessState -> {
  694. val bundle = Bundle()
  695. bundle.putString(KEY_ROOM_TOKEN, state.roomOverall.ocs!!.data!!.token)
  696. // bundle.putString(KEY_ROOM_ID, state.roomOverall.ocs!!.data!!.roomId)
  697. leaveRoom {
  698. val chatIntent = Intent(context, ChatActivity::class.java)
  699. chatIntent.putExtras(bundle)
  700. chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
  701. startActivity(chatIntent)
  702. }
  703. }
  704. is ChatViewModel.CreateRoomErrorState -> {
  705. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  706. }
  707. else -> {}
  708. }
  709. }
  710. chatViewModel.chatMessageViewState.observe(this) { state ->
  711. when (state) {
  712. is ChatViewModel.ChatMessageStartState -> {
  713. // Handle UI on first load
  714. cancelNotificationsForCurrentConversation()
  715. binding.progressBar.visibility = View.GONE
  716. binding.messagesListView.visibility = View.VISIBLE
  717. collapseSystemMessages()
  718. }
  719. is ChatViewModel.ChatMessageUpdateState -> {
  720. // unused atm
  721. }
  722. is ChatViewModel.ChatMessageErrorState -> {
  723. // unused atm
  724. }
  725. else -> {}
  726. }
  727. }
  728. this.lifecycleScope.launch {
  729. chatViewModel.getMessageFlow
  730. .onEach { triple ->
  731. val lookIntoFuture = triple.first
  732. val setUnreadMessagesMarker = triple.second
  733. var chatMessageList = triple.third
  734. chatMessageList = handleSystemMessages(chatMessageList)
  735. determinePreviousMessageIds(chatMessageList)
  736. handleExpandableSystemMessages(chatMessageList)
  737. if (chatMessageList.isNotEmpty() &&
  738. ChatMessage.SystemMessageType.CLEARED_CHAT == chatMessageList[0].systemMessageType
  739. ) {
  740. adapter?.clear()
  741. adapter?.notifyDataSetChanged()
  742. }
  743. if (lookIntoFuture) {
  744. Log.d(TAG, "chatMessageList.size in getMessageFlow:" + chatMessageList.size)
  745. processMessagesFromTheFuture(chatMessageList, setUnreadMessagesMarker)
  746. } else {
  747. processMessagesNotFromTheFuture(chatMessageList)
  748. collapseSystemMessages()
  749. }
  750. processExpiredMessages()
  751. processCallStartedMessages(chatMessageList)
  752. adapter?.notifyDataSetChanged()
  753. }
  754. .collect()
  755. }
  756. this.lifecycleScope.launch {
  757. chatViewModel.getUpdateMessageFlow
  758. .onEach {
  759. updateMessageInsideAdapter(it)
  760. }
  761. .collect()
  762. }
  763. this.lifecycleScope.launch {
  764. chatViewModel.getLastCommonReadFlow
  765. .onEach {
  766. updateReadStatusOfAllMessages(it)
  767. processExpiredMessages()
  768. }
  769. .collect()
  770. }
  771. this.lifecycleScope.launch {
  772. chatViewModel.getLastReadMessageFlow
  773. .onEach { lastRead ->
  774. scrollToAndCenterMessageWithId(lastRead.toString())
  775. }
  776. .collect()
  777. }
  778. chatViewModel.reactionDeletedViewState.observe(this) { state ->
  779. when (state) {
  780. is ChatViewModel.ReactionDeletedSuccessState -> {
  781. updateUiToDeleteReaction(
  782. state.reactionDeletedModel.chatMessage,
  783. state.reactionDeletedModel.emoji
  784. )
  785. }
  786. else -> {}
  787. }
  788. }
  789. chatViewModel.reactionAddedViewState.observe(this) { state ->
  790. when (state) {
  791. is ChatViewModel.ReactionAddedSuccessState -> {
  792. updateUiToAddReaction(
  793. state.reactionAddedModel.chatMessage,
  794. state.reactionAddedModel.emoji
  795. )
  796. }
  797. else -> {}
  798. }
  799. }
  800. messageInputViewModel.editMessageViewState.observe(this) { state ->
  801. when (state) {
  802. is MessageInputViewModel.EditMessageSuccessState -> {
  803. when (state.messageEdited.ocs?.meta?.statusCode) {
  804. HTTP_BAD_REQUEST -> {
  805. Snackbar.make(
  806. binding.root,
  807. getString(R.string.edit_error_24_hours_old_message),
  808. Snackbar.LENGTH_LONG
  809. ).show()
  810. }
  811. HTTP_FORBIDDEN -> {
  812. Snackbar.make(
  813. binding.root,
  814. getString(R.string.conversation_is_read_only),
  815. Snackbar.LENGTH_LONG
  816. ).show()
  817. }
  818. HTTP_NOT_FOUND -> {
  819. Snackbar.make(
  820. binding.root,
  821. "Conversation not found",
  822. Snackbar.LENGTH_LONG
  823. ).show()
  824. }
  825. }
  826. val newString = state.messageEdited.ocs?.data?.parentMessage?.message ?: "(null)"
  827. val id = state.messageEdited.ocs?.data?.parentMessage?.id.toString()
  828. val index = adapter?.getMessagePositionById(id) ?: 0
  829. val message = adapter?.items?.get(index)?.item as ChatMessage
  830. setMessageAsEdited(message, newString)
  831. }
  832. is MessageInputViewModel.EditMessageErrorState -> {
  833. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  834. }
  835. else -> {}
  836. }
  837. }
  838. chatViewModel.getVoiceRecordingLocked.observe(this) { showContiniousVoiceRecording ->
  839. if (showContiniousVoiceRecording) {
  840. binding.voiceRecordingLock.visibility = View.GONE
  841. supportFragmentManager.commit {
  842. setReorderingAllowed(true) // apparently used for optimizations
  843. replace(R.id.fragment_container_activity_chat, MessageInputVoiceRecordingFragment())
  844. }
  845. } else {
  846. supportFragmentManager.commit {
  847. setReorderingAllowed(true)
  848. replace(R.id.fragment_container_activity_chat, MessageInputFragment())
  849. }
  850. }
  851. }
  852. chatViewModel.getVoiceRecordingInProgress.observe(this) { voiceRecordingInProgress ->
  853. VibrationUtils.vibrateShort(context)
  854. binding.voiceRecordingLock.visibility = if (
  855. voiceRecordingInProgress &&
  856. chatViewModel.getVoiceRecordingLocked.value != true
  857. ) {
  858. View.VISIBLE
  859. } else {
  860. View.GONE
  861. }
  862. }
  863. chatViewModel.recordTouchObserver.observe(this) { y ->
  864. binding.voiceRecordingLock.y -= y
  865. }
  866. }
  867. private fun removeUnreadMessagesMarker() {
  868. val index = adapter?.getMessagePositionById(UNREAD_MESSAGES_MARKER_ID.toString())
  869. if (index != null && index != -1) {
  870. adapter?.items?.removeAt(index)
  871. }
  872. }
  873. @Suppress("Detekt.TooGenericExceptionCaught")
  874. override fun onResume() {
  875. super.onResume()
  876. logConversationInfos("onResume")
  877. pullChatMessagesPending = false
  878. webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
  879. webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
  880. cancelNotificationsForCurrentConversation()
  881. chatViewModel.getRoom(conversationUser!!, roomToken)
  882. actionBar?.show()
  883. setupSwipeToReply()
  884. binding.unreadMessagesPopup.setOnClickListener {
  885. binding.messagesListView.smoothScrollToPosition(0)
  886. binding.unreadMessagesPopup.visibility = View.GONE
  887. }
  888. binding.scrollDownButton.setOnClickListener {
  889. binding.messagesListView.scrollToPosition(0)
  890. it.visibility = View.GONE
  891. }
  892. binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryTonal(it.scrollDownButton) }
  893. binding.let { viewThemeUtils.material.themeFAB(it.voiceRecordingLock) }
  894. binding.let { viewThemeUtils.material.colorMaterialButtonPrimaryFilled(it.unreadMessagesPopup) }
  895. binding.messagesListView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
  896. override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
  897. super.onScrollStateChanged(recyclerView, newState)
  898. if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
  899. if (isScrolledToBottom()) {
  900. binding.unreadMessagesPopup.visibility = View.GONE
  901. binding.scrollDownButton.visibility = View.GONE
  902. } else {
  903. if (binding.unreadMessagesPopup.isShown) {
  904. binding.scrollDownButton.visibility = View.GONE
  905. } else {
  906. binding.scrollDownButton.visibility = View.VISIBLE
  907. }
  908. }
  909. }
  910. }
  911. })
  912. loadAvatarForStatusBar()
  913. setActionBarTitle()
  914. viewThemeUtils.material.colorToolbarOverflowIcon(binding.chatToolbar)
  915. }
  916. private fun getLastAdapterId(): Int {
  917. var lastId = 0
  918. if (adapter?.items?.size != 0) {
  919. val item = adapter?.items?.get(0)?.item
  920. if (item != null) {
  921. lastId = (item as ChatMessage).jsonMessageId
  922. } else {
  923. lastId = 0
  924. }
  925. }
  926. return lastId
  927. }
  928. private fun setupActionBar() {
  929. setSupportActionBar(binding.chatToolbar)
  930. binding.chatToolbar.setNavigationOnClickListener {
  931. onBackPressedDispatcher.onBackPressed()
  932. }
  933. supportActionBar?.setDisplayHomeAsUpEnabled(true)
  934. supportActionBar?.setDisplayShowHomeEnabled(true)
  935. supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(R.color.transparent, null)))
  936. setActionBarTitle()
  937. viewThemeUtils.material.themeToolbar(binding.chatToolbar)
  938. }
  939. private fun initAdapter() {
  940. val senderId = if (!conversationUser!!.userId.equals("?")) {
  941. "users/" + conversationUser!!.userId
  942. } else {
  943. currentConversation?.actorType + "/" + currentConversation?.actorId
  944. }
  945. Log.d(TAG, "Initialize TalkMessagesListAdapter with senderId: $senderId")
  946. adapter = TalkMessagesListAdapter(
  947. senderId,
  948. initMessageHolders(),
  949. ImageLoader { imageView, url, placeholder ->
  950. imageView.loadAvatarOrImagePreview(url!!, conversationUser!!, placeholder as Drawable?)
  951. },
  952. this
  953. )
  954. adapter?.setLoadMoreListener(this)
  955. adapter?.setDateHeadersFormatter { format(it) }
  956. adapter?.setOnMessageViewLongClickListener { view, message -> onMessageViewLongClick(view, message) }
  957. adapter?.registerViewClickListener(
  958. R.id.playPauseBtn
  959. ) { _, message ->
  960. val filename = message.selectedIndividualHashMap!!["name"]
  961. val file = File(context.cacheDir, filename!!)
  962. if (file.exists()) {
  963. if (message.isPlayingVoiceMessage) {
  964. pausePlayback(message)
  965. } else {
  966. val retrieved = appPreferences.getWaveFormFromFile(filename)
  967. if (retrieved.isEmpty()) {
  968. setUpWaveform(message)
  969. } else {
  970. startPlayback(message)
  971. }
  972. }
  973. } else {
  974. Log.d(TAG, "Downloaded to cache")
  975. downloadFileToCache(message, true) {
  976. setUpWaveform(message)
  977. }
  978. }
  979. }
  980. }
  981. private fun setUpWaveform(message: ChatMessage, thenPlay: Boolean = true) {
  982. val filename = message.selectedIndividualHashMap!!["name"]
  983. val file = File(context.cacheDir, filename!!)
  984. if (file.exists() && message.voiceMessageFloatArray == null) {
  985. message.isDownloadingVoiceMessage = true
  986. adapter?.update(message)
  987. CoroutineScope(Dispatchers.Default).launch {
  988. val r = AudioUtils.audioFileToFloatArray(file)
  989. appPreferences.saveWaveFormForFile(filename, r.toTypedArray())
  990. message.voiceMessageFloatArray = r
  991. withContext(Dispatchers.Main) {
  992. startPlayback(message, thenPlay)
  993. }
  994. }
  995. } else {
  996. startPlayback(message, thenPlay)
  997. }
  998. }
  999. private fun initMessageHolders(): MessageHolders {
  1000. val messageHolders = MessageHolders()
  1001. val profileBottomSheet = ProfileBottomSheet(ncApi, conversationUser!!, viewThemeUtils)
  1002. val payload = MessagePayload(
  1003. roomToken,
  1004. ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!),
  1005. profileBottomSheet
  1006. )
  1007. messageHolders.setIncomingTextConfig(
  1008. IncomingTextMessageViewHolder::class.java,
  1009. R.layout.item_custom_incoming_text_message,
  1010. payload
  1011. )
  1012. messageHolders.setOutcomingTextConfig(
  1013. OutcomingTextMessageViewHolder::class.java,
  1014. R.layout.item_custom_outcoming_text_message
  1015. )
  1016. messageHolders.setIncomingImageConfig(
  1017. IncomingPreviewMessageViewHolder::class.java,
  1018. R.layout.item_custom_incoming_preview_message,
  1019. payload
  1020. )
  1021. messageHolders.setOutcomingImageConfig(
  1022. OutcomingPreviewMessageViewHolder::class.java,
  1023. R.layout.item_custom_outcoming_preview_message
  1024. )
  1025. messageHolders.registerContentType(
  1026. CONTENT_TYPE_CALL_STARTED,
  1027. CallStartedViewHolder::class.java,
  1028. payload,
  1029. R.layout.call_started_message,
  1030. CallStartedViewHolder::class.java,
  1031. payload,
  1032. R.layout.call_started_message,
  1033. this
  1034. )
  1035. messageHolders.registerContentType(
  1036. CONTENT_TYPE_TEMP,
  1037. TemporaryMessageViewHolder::class.java,
  1038. payload,
  1039. R.layout.item_temporary_message,
  1040. TemporaryMessageViewHolder::class.java,
  1041. payload,
  1042. R.layout.item_temporary_message,
  1043. this
  1044. )
  1045. messageHolders.registerContentType(
  1046. CONTENT_TYPE_SYSTEM_MESSAGE,
  1047. SystemMessageViewHolder::class.java,
  1048. R.layout.item_system_message,
  1049. SystemMessageViewHolder::class.java,
  1050. R.layout.item_system_message,
  1051. this
  1052. )
  1053. messageHolders.registerContentType(
  1054. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE,
  1055. UnreadNoticeMessageViewHolder::class.java,
  1056. R.layout.item_date_header,
  1057. UnreadNoticeMessageViewHolder::class.java,
  1058. R.layout.item_date_header,
  1059. this
  1060. )
  1061. messageHolders.registerContentType(
  1062. CONTENT_TYPE_LOCATION,
  1063. IncomingLocationMessageViewHolder::class.java,
  1064. payload,
  1065. R.layout.item_custom_incoming_location_message,
  1066. OutcomingLocationMessageViewHolder::class.java,
  1067. null,
  1068. R.layout.item_custom_outcoming_location_message,
  1069. this
  1070. )
  1071. messageHolders.registerContentType(
  1072. CONTENT_TYPE_VOICE_MESSAGE,
  1073. IncomingVoiceMessageViewHolder::class.java,
  1074. payload,
  1075. R.layout.item_custom_incoming_voice_message,
  1076. OutcomingVoiceMessageViewHolder::class.java,
  1077. null,
  1078. R.layout.item_custom_outcoming_voice_message,
  1079. this
  1080. )
  1081. messageHolders.registerContentType(
  1082. CONTENT_TYPE_POLL,
  1083. IncomingPollMessageViewHolder::class.java,
  1084. payload,
  1085. R.layout.item_custom_incoming_poll_message,
  1086. OutcomingPollMessageViewHolder::class.java,
  1087. payload,
  1088. R.layout.item_custom_outcoming_poll_message,
  1089. this
  1090. )
  1091. messageHolders.registerContentType(
  1092. CONTENT_TYPE_LINK_PREVIEW,
  1093. IncomingLinkPreviewMessageViewHolder::class.java,
  1094. payload,
  1095. R.layout.item_custom_incoming_link_preview_message,
  1096. OutcomingLinkPreviewMessageViewHolder::class.java,
  1097. payload,
  1098. R.layout.item_custom_outcoming_link_preview_message,
  1099. this
  1100. )
  1101. messageHolders.registerContentType(
  1102. CONTENT_TYPE_DECK_CARD,
  1103. IncomingDeckCardViewHolder::class.java,
  1104. payload,
  1105. R.layout.item_custom_incoming_deck_card_message,
  1106. OutcomingDeckCardViewHolder::class.java,
  1107. payload,
  1108. R.layout.item_custom_outcoming_deck_card_message,
  1109. this
  1110. )
  1111. return messageHolders
  1112. }
  1113. @Suppress("MagicNumber", "LongMethod")
  1114. private fun updateTypingIndicator() {
  1115. fun ellipsize(text: String): String = DisplayUtils.ellipsize(text, TYPING_INDICATOR_MAX_NAME_LENGTH)
  1116. val participantNames = ArrayList<String>()
  1117. for (typingParticipant in typingParticipants.values) {
  1118. participantNames.add(typingParticipant.name)
  1119. }
  1120. val typingString: SpannableStringBuilder
  1121. when (typingParticipants.size) {
  1122. 0 -> typingString = SpannableStringBuilder().append(binding.typingIndicator.text)
  1123. // person1 is typing
  1124. 1 -> typingString = SpannableStringBuilder()
  1125. .bold { append(ellipsize(participantNames[0])) }
  1126. .append(WHITESPACE + context.resources?.getString(R.string.typing_is_typing))
  1127. // person1 and person2 are typing
  1128. 2 -> typingString = SpannableStringBuilder()
  1129. .bold { append(ellipsize(participantNames[0])) }
  1130. .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE)
  1131. .bold { append(ellipsize(participantNames[1])) }
  1132. .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing))
  1133. // person1, person2 and person3 are typing
  1134. 3 -> typingString = SpannableStringBuilder()
  1135. .bold { append(ellipsize(participantNames[0])) }
  1136. .append(COMMA)
  1137. .bold { append(ellipsize(participantNames[1])) }
  1138. .append(WHITESPACE + context.resources?.getString(R.string.nc_common_and) + WHITESPACE)
  1139. .bold { append(ellipsize(participantNames[2])) }
  1140. .append(WHITESPACE + context.resources?.getString(R.string.typing_are_typing))
  1141. // person1, person2, person3 and 1 other is typing
  1142. 4 -> typingString = SpannableStringBuilder()
  1143. .bold { append(participantNames[0]) }
  1144. .append(COMMA)
  1145. .bold { append(participantNames[1]) }
  1146. .append(COMMA)
  1147. .bold { append(participantNames[2]) }
  1148. .append(WHITESPACE + context.resources?.getString(R.string.typing_1_other))
  1149. // person1, person2, person3 and x others are typing
  1150. else -> {
  1151. val moreTypersAmount = typingParticipants.size - 3
  1152. val othersTyping = context.resources?.getString(R.string.typing_x_others)?.let {
  1153. String.format(it, moreTypersAmount)
  1154. }
  1155. typingString = SpannableStringBuilder()
  1156. .bold { append(participantNames[0]) }
  1157. .append(COMMA)
  1158. .bold { append(participantNames[1]) }
  1159. .append(COMMA)
  1160. .bold { append(participantNames[2]) }
  1161. .append(othersTyping)
  1162. }
  1163. }
  1164. runOnUiThread {
  1165. binding.typingIndicator.text = typingString
  1166. val typingIndicatorPositionY = if (participantNames.size > 0) {
  1167. TYPING_INDICATOR_POSITION_VISIBLE
  1168. } else {
  1169. TYPING_INDICATOR_POSITION_HIDDEN
  1170. }
  1171. binding.typingIndicatorWrapper.animate()
  1172. .translationY(DisplayUtils.convertDpToPixel(typingIndicatorPositionY, context))
  1173. .setInterpolator(AccelerateDecelerateInterpolator())
  1174. .duration = TYPING_INDICATOR_ANIMATION_DURATION
  1175. }
  1176. }
  1177. private fun isTypingStatusEnabled(): Boolean =
  1178. webSocketInstance != null &&
  1179. !CapabilitiesUtil.isTypingStatusPrivate(conversationUser!!)
  1180. private fun setupSwipeToReply() {
  1181. if (this::participantPermissions.isInitialized &&
  1182. participantPermissions.hasChatPermission() &&
  1183. !isReadOnlyConversation()
  1184. ) {
  1185. val messageSwipeCallback = MessageSwipeCallback(
  1186. this,
  1187. object : MessageSwipeActions {
  1188. override fun showReplyUI(position: Int) {
  1189. val chatMessage = adapter?.items?.getOrNull(position)?.item as ChatMessage?
  1190. if (chatMessage != null) {
  1191. messageInputViewModel.reply(chatMessage)
  1192. }
  1193. }
  1194. }
  1195. )
  1196. val itemTouchHelper = ItemTouchHelper(messageSwipeCallback)
  1197. itemTouchHelper.attachToRecyclerView(binding.messagesListView)
  1198. }
  1199. }
  1200. private fun loadAvatarForStatusBar() {
  1201. if (currentConversation == null) {
  1202. return
  1203. }
  1204. if (isOneToOneConversation()) {
  1205. var url = ApiUtils.getUrlForAvatar(
  1206. conversationUser!!.baseUrl!!,
  1207. currentConversation!!.name,
  1208. true
  1209. )
  1210. if (DisplayUtils.isDarkModeOn(supportActionBar?.themedContext!!)) {
  1211. url = "$url/dark"
  1212. }
  1213. val target = object : Target {
  1214. private fun setIcon(drawable: Drawable?) {
  1215. supportActionBar?.let {
  1216. val avatarSize = (it.height / TOOLBAR_AVATAR_RATIO).roundToInt()
  1217. val size = DisplayUtils.convertDpToPixel(STATUS_SIZE_IN_DP, context)
  1218. if (drawable != null && avatarSize > 0) {
  1219. val bitmap = drawable.toBitmap(avatarSize, avatarSize)
  1220. val status = StatusDrawable(
  1221. currentConversation!!.status,
  1222. null,
  1223. size,
  1224. 0,
  1225. binding.chatToolbar.context
  1226. )
  1227. viewThemeUtils.talk.themeStatusDrawable(context, status)
  1228. binding.chatToolbar.findViewById<ImageView>(R.id.chat_toolbar_avatar)
  1229. .setImageDrawable(BitmapDrawable(resources, bitmap))
  1230. binding.chatToolbar.findViewById<ImageView>(R.id.chat_toolbar_status)
  1231. .setImageDrawable(status)
  1232. binding.chatToolbar.findViewById<ImageView>(R.id.chat_toolbar_status).contentDescription =
  1233. currentConversation?.status
  1234. binding.chatToolbar.findViewById<FrameLayout>(R.id.chat_toolbar_avatar_container)
  1235. .visibility = View.VISIBLE
  1236. } else {
  1237. Log.d(TAG, "loadAvatarForStatusBar avatarSize <= 0")
  1238. }
  1239. }
  1240. }
  1241. override fun onStart(placeholder: Drawable?) {
  1242. this.setIcon(placeholder)
  1243. }
  1244. override fun onSuccess(result: Drawable) {
  1245. this.setIcon(result)
  1246. }
  1247. }
  1248. val credentials = ApiUtils.getCredentials(conversationUser!!.username, conversationUser!!.token)
  1249. if (credentials != null) {
  1250. context.imageLoader.enqueue(
  1251. ImageRequest.Builder(context)
  1252. .data(url)
  1253. .addHeader("Authorization", credentials)
  1254. .transformations(CircleCropTransformation())
  1255. .crossfade(true)
  1256. .target(target)
  1257. .memoryCachePolicy(CachePolicy.DISABLED)
  1258. .diskCachePolicy(CachePolicy.DISABLED)
  1259. .build()
  1260. )
  1261. }
  1262. } else {
  1263. binding.chatToolbar.findViewById<FrameLayout>(R.id.chat_toolbar_avatar_container).visibility = View.GONE
  1264. }
  1265. }
  1266. fun isOneToOneConversation() =
  1267. currentConversation != null &&
  1268. currentConversation?.type != null &&
  1269. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  1270. private fun isGroupConversation() =
  1271. currentConversation != null &&
  1272. currentConversation?.type != null &&
  1273. currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL
  1274. private fun isPublicConversation() =
  1275. currentConversation != null &&
  1276. currentConversation?.type != null &&
  1277. currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
  1278. private fun updateRoomTimerHandler() {
  1279. val delayForRecursiveCall = if (shouldShowLobby()) {
  1280. GET_ROOM_INFO_DELAY_LOBBY
  1281. } else {
  1282. GET_ROOM_INFO_DELAY_NORMAL
  1283. }
  1284. if (getRoomInfoTimerHandler == null) {
  1285. getRoomInfoTimerHandler = Handler()
  1286. }
  1287. getRoomInfoTimerHandler?.postDelayed(
  1288. {
  1289. chatViewModel.getRoom(conversationUser!!, roomToken)
  1290. },
  1291. delayForRecursiveCall
  1292. )
  1293. }
  1294. private fun switchToRoom(token: String, startCallAfterRoomSwitch: Boolean, isVoiceOnlyCall: Boolean) {
  1295. if (conversationUser != null) {
  1296. runOnUiThread {
  1297. if (currentConversation?.objectType == ConversationEnums.ObjectType.ROOM) {
  1298. Snackbar.make(
  1299. binding.root,
  1300. context.resources.getString(R.string.switch_to_main_room),
  1301. Snackbar.LENGTH_LONG
  1302. ).show()
  1303. } else {
  1304. Snackbar.make(
  1305. binding.root,
  1306. context.resources.getString(R.string.switch_to_breakout_room),
  1307. Snackbar.LENGTH_LONG
  1308. ).show()
  1309. }
  1310. }
  1311. val bundle = Bundle()
  1312. bundle.putString(KEY_ROOM_TOKEN, token)
  1313. if (startCallAfterRoomSwitch) {
  1314. bundle.putBoolean(KEY_START_CALL_AFTER_ROOM_SWITCH, true)
  1315. bundle.putBoolean(KEY_CALL_VOICE_ONLY, isVoiceOnlyCall)
  1316. }
  1317. leaveRoom {
  1318. val chatIntent = Intent(context, ChatActivity::class.java)
  1319. chatIntent.putExtras(bundle)
  1320. chatIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
  1321. startActivity(chatIntent)
  1322. }
  1323. }
  1324. }
  1325. private fun showCallButtonMenu(isVoiceOnlyCall: Boolean) {
  1326. val anchor: View? = if (isVoiceOnlyCall) {
  1327. findViewById(R.id.conversation_voice_call)
  1328. } else {
  1329. findViewById(R.id.conversation_video_call)
  1330. }
  1331. if (anchor != null) {
  1332. val popupMenu = PopupMenu(
  1333. ContextThemeWrapper(this, R.style.CallButtonMenu),
  1334. anchor,
  1335. Gravity.END
  1336. )
  1337. popupMenu.inflate(R.menu.chat_call_menu)
  1338. popupMenu.setOnMenuItemClickListener { item: MenuItem ->
  1339. when (item.itemId) {
  1340. R.id.call_without_notification -> startACall(isVoiceOnlyCall, true)
  1341. }
  1342. true
  1343. }
  1344. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
  1345. popupMenu.setForceShowIcon(true)
  1346. }
  1347. popupMenu.show()
  1348. }
  1349. }
  1350. private fun startPlayback(message: ChatMessage, doPlay: Boolean = true) {
  1351. if (!active) {
  1352. // don't begin to play voice message if screen is not visible anymore.
  1353. // this situation might happen if file is downloading but user already left the chatview.
  1354. // If user returns to chatview, the old chatview instance is not attached anymore
  1355. // and he has to click the play button again (which is considered to be okay)
  1356. return
  1357. }
  1358. initMediaPlayer(message)
  1359. mediaPlayer?.let {
  1360. if (!it.isPlaying && doPlay) {
  1361. chatViewModel.audioRequest(true) {
  1362. it.start()
  1363. }
  1364. }
  1365. mediaPlayerHandler = Handler()
  1366. runOnUiThread(object : Runnable {
  1367. override fun run() {
  1368. if (mediaPlayer != null) {
  1369. if (message.isPlayingVoiceMessage) {
  1370. val pos = mediaPlayer!!.currentPosition / VOICE_MESSAGE_SEEKBAR_BASE
  1371. if (pos < (mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE)) {
  1372. lastRecordMediaPosition = mediaPlayer!!.currentPosition
  1373. message.voiceMessagePlayedSeconds = pos
  1374. message.voiceMessageSeekbarProgress = mediaPlayer!!.currentPosition
  1375. adapter?.update(message)
  1376. } else {
  1377. message.resetVoiceMessage = true
  1378. message.voiceMessagePlayedSeconds = 0
  1379. message.voiceMessageSeekbarProgress = 0
  1380. adapter?.update(message)
  1381. stopMediaPlayer(message)
  1382. }
  1383. }
  1384. }
  1385. mediaPlayerHandler?.postDelayed(this, MILISEC_15)
  1386. }
  1387. })
  1388. message.isDownloadingVoiceMessage = false
  1389. message.isPlayingVoiceMessage = doPlay
  1390. // message.voiceMessagePlayedSeconds = lastRecordMediaPosition / VOICE_MESSAGE_SEEKBAR_BASE
  1391. // message.voiceMessageSeekbarProgress = lastRecordMediaPosition
  1392. // the commented instructions objective was to update audio seekbarprogress
  1393. // in the case in which audio status is paused when the position is resumed
  1394. adapter?.update(message)
  1395. }
  1396. }
  1397. private fun pausePlayback(message: ChatMessage) {
  1398. if (mediaPlayer!!.isPlaying) {
  1399. chatViewModel.audioRequest(false) {
  1400. mediaPlayer!!.pause()
  1401. }
  1402. }
  1403. message.isPlayingVoiceMessage = false
  1404. adapter?.update(message)
  1405. }
  1406. @Suppress("Detekt.TooGenericExceptionCaught")
  1407. private fun initMediaPlayer(message: ChatMessage) {
  1408. if (message != currentlyPlayedVoiceMessage) {
  1409. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) }
  1410. }
  1411. if (mediaPlayer == null) {
  1412. val fileName = message.selectedIndividualHashMap!!["name"]
  1413. val absolutePath = context.cacheDir.absolutePath + "/" + fileName
  1414. try {
  1415. mediaPlayer = MediaPlayer().apply {
  1416. setDataSource(absolutePath)
  1417. prepare()
  1418. setOnPreparedListener {
  1419. currentlyPlayedVoiceMessage = message
  1420. message.voiceMessageDuration = mediaPlayer!!.duration / VOICE_MESSAGE_SEEKBAR_BASE
  1421. lastRecordedSeeked = false
  1422. }
  1423. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  1424. setOnMediaTimeDiscontinuityListener { mp, _ ->
  1425. if (lastRecordMediaPosition > ONE_SECOND_IN_MILLIS && !lastRecordedSeeked) {
  1426. mp.seekTo(lastRecordMediaPosition)
  1427. lastRecordedSeeked = true
  1428. }
  1429. }
  1430. // this ensures that audio can be resumed at a given position
  1431. this.seekTo(lastRecordMediaPosition)
  1432. }
  1433. setOnCompletionListener {
  1434. stopMediaPlayer(message)
  1435. }
  1436. }
  1437. } catch (e: Exception) {
  1438. Log.e(TAG, "failed to initialize mediaPlayer", e)
  1439. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  1440. }
  1441. }
  1442. }
  1443. private fun stopMediaPlayer(message: ChatMessage) {
  1444. message.isPlayingVoiceMessage = false
  1445. message.resetVoiceMessage = true
  1446. adapter?.update(message)
  1447. currentlyPlayedVoiceMessage = null
  1448. lastRecordMediaPosition = 0 // this ensures that if audio track is changed, then it is played from the beginning
  1449. mediaPlayerHandler?.removeCallbacksAndMessages(null)
  1450. try {
  1451. mediaPlayer?.let {
  1452. if (it.isPlaying) {
  1453. Log.d(TAG, "media player is stopped")
  1454. chatViewModel.audioRequest(false) {
  1455. it.stop()
  1456. }
  1457. }
  1458. }
  1459. } catch (e: IllegalStateException) {
  1460. Log.e(TAG, "mediaPlayer was not initialized", e)
  1461. } finally {
  1462. mediaPlayer?.release()
  1463. mediaPlayer = null
  1464. }
  1465. }
  1466. override fun updateMediaPlayerProgressBySlider(messageWithSlidedProgress: ChatMessage, progress: Int) {
  1467. if (mediaPlayer != null) {
  1468. if (messageWithSlidedProgress == currentlyPlayedVoiceMessage) {
  1469. mediaPlayer!!.seekTo(progress)
  1470. }
  1471. }
  1472. }
  1473. @SuppressLint("NotifyDataSetChanged")
  1474. override fun collapseSystemMessages() {
  1475. adapter?.items?.forEach {
  1476. if (it.item is ChatMessage) {
  1477. val chatMessage = it.item as ChatMessage
  1478. if (isChildOfExpandableSystemMessage(chatMessage)) {
  1479. chatMessage.hiddenByCollapse = true
  1480. }
  1481. chatMessage.isExpanded = false
  1482. }
  1483. }
  1484. adapter?.notifyDataSetChanged()
  1485. }
  1486. private fun isChildOfExpandableSystemMessage(chatMessage: ChatMessage): Boolean =
  1487. isSystemMessage(chatMessage) &&
  1488. !chatMessage.expandableParent &&
  1489. chatMessage.lastItemOfExpandableGroup != 0
  1490. @SuppressLint("NotifyDataSetChanged")
  1491. override fun expandSystemMessage(chatMessageToExpand: ChatMessage) {
  1492. adapter?.items?.forEach {
  1493. if (it.item is ChatMessage) {
  1494. val belongsToGroupToExpand =
  1495. (it.item as ChatMessage).lastItemOfExpandableGroup == chatMessageToExpand.lastItemOfExpandableGroup
  1496. if (belongsToGroupToExpand) {
  1497. (it.item as ChatMessage).hiddenByCollapse = false
  1498. }
  1499. }
  1500. }
  1501. chatMessageToExpand.isExpanded = true
  1502. adapter?.notifyDataSetChanged()
  1503. }
  1504. @SuppressLint("LongLogTag")
  1505. private fun downloadFileToCache(
  1506. message: ChatMessage,
  1507. openWhenDownloaded: Boolean,
  1508. funToCallWhenDownloadSuccessful: (() -> Unit)
  1509. ) {
  1510. message.isDownloadingVoiceMessage = true
  1511. message.openWhenDownloaded = openWhenDownloaded
  1512. adapter?.update(message)
  1513. val baseUrl = message.activeUser!!.baseUrl
  1514. val userId = message.activeUser!!.userId
  1515. val attachmentFolder = CapabilitiesUtil.getAttachmentFolder(
  1516. message.activeUser!!.capabilities!!
  1517. .spreedCapability!!
  1518. )
  1519. val fileName = message.selectedIndividualHashMap!!["name"]
  1520. var size = message.selectedIndividualHashMap!!["size"]
  1521. if (size == null) {
  1522. size = "-1"
  1523. }
  1524. val fileSize = size.toLong()
  1525. val fileId = message.selectedIndividualHashMap!!["id"]
  1526. val path = message.selectedIndividualHashMap!!["path"]
  1527. // check if download worker is already running
  1528. val workers = WorkManager.getInstance(
  1529. context
  1530. ).getWorkInfosByTag(fileId!!)
  1531. try {
  1532. for (workInfo in workers.get()) {
  1533. if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
  1534. Log.d(TAG, "Download worker for $fileId is already running or scheduled")
  1535. return
  1536. }
  1537. }
  1538. } catch (e: ExecutionException) {
  1539. Log.e(TAG, "Error when checking if worker already exists", e)
  1540. } catch (e: InterruptedException) {
  1541. Log.e(TAG, "Error when checking if worker already exists", e)
  1542. }
  1543. val data: Data = Data.Builder()
  1544. .putString(DownloadFileToCacheWorker.KEY_BASE_URL, baseUrl)
  1545. .putString(DownloadFileToCacheWorker.KEY_USER_ID, userId)
  1546. .putString(DownloadFileToCacheWorker.KEY_ATTACHMENT_FOLDER, attachmentFolder)
  1547. .putString(DownloadFileToCacheWorker.KEY_FILE_NAME, fileName)
  1548. .putString(DownloadFileToCacheWorker.KEY_FILE_PATH, path)
  1549. .putLong(DownloadFileToCacheWorker.KEY_FILE_SIZE, fileSize)
  1550. .build()
  1551. val downloadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(DownloadFileToCacheWorker::class.java)
  1552. .setInputData(data)
  1553. .addTag(fileId)
  1554. .build()
  1555. WorkManager.getInstance().enqueue(downloadWorker)
  1556. WorkManager.getInstance(context).getWorkInfoByIdLiveData(downloadWorker.id)
  1557. .observeForever { workInfo: WorkInfo ->
  1558. if (workInfo.state == WorkInfo.State.SUCCEEDED) {
  1559. funToCallWhenDownloadSuccessful()
  1560. }
  1561. }
  1562. }
  1563. fun isRecordAudioPermissionGranted(): Boolean =
  1564. PermissionChecker.checkSelfPermission(
  1565. context,
  1566. Manifest.permission.RECORD_AUDIO
  1567. ) == PERMISSION_GRANTED
  1568. fun requestRecordAudioPermissions() {
  1569. requestPermissions(
  1570. arrayOf(
  1571. Manifest.permission.RECORD_AUDIO
  1572. ),
  1573. REQUEST_RECORD_AUDIO_PERMISSION
  1574. )
  1575. }
  1576. private fun requestCameraPermissions() {
  1577. requestPermissions(
  1578. arrayOf(
  1579. Manifest.permission.CAMERA
  1580. ),
  1581. REQUEST_CAMERA_PERMISSION
  1582. )
  1583. }
  1584. private fun requestReadContacts() {
  1585. requestPermissions(
  1586. arrayOf(
  1587. Manifest.permission.READ_CONTACTS
  1588. ),
  1589. REQUEST_READ_CONTACT_PERMISSION
  1590. )
  1591. }
  1592. private fun requestReadFilesPermissions() {
  1593. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
  1594. requestPermissions(
  1595. arrayOf(
  1596. Manifest.permission.READ_MEDIA_IMAGES,
  1597. Manifest.permission.READ_MEDIA_VIDEO,
  1598. Manifest.permission.READ_MEDIA_AUDIO
  1599. ),
  1600. REQUEST_SHARE_FILE_PERMISSION
  1601. )
  1602. } else {
  1603. requestPermissions(
  1604. arrayOf(
  1605. Manifest.permission.READ_EXTERNAL_STORAGE
  1606. ),
  1607. REQUEST_SHARE_FILE_PERMISSION
  1608. )
  1609. }
  1610. }
  1611. private fun checkShowCallButtons() {
  1612. if (isReadOnlyConversation() ||
  1613. shouldShowLobby() ||
  1614. ConversationUtils.isNoteToSelfConversation(currentConversation)
  1615. ) {
  1616. disableCallButtons()
  1617. } else {
  1618. enableCallButtons()
  1619. }
  1620. }
  1621. private fun checkShowMessageInputView() {
  1622. if (isReadOnlyConversation() ||
  1623. shouldShowLobby() ||
  1624. !participantPermissions.hasChatPermission()
  1625. ) {
  1626. binding.fragmentContainerActivityChat.visibility = View.GONE
  1627. } else {
  1628. binding.fragmentContainerActivityChat.visibility = View.VISIBLE
  1629. }
  1630. }
  1631. private fun shouldShowLobby(): Boolean {
  1632. if (currentConversation != null) {
  1633. return CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.WEBINARY_LOBBY) &&
  1634. currentConversation?.lobbyState == ConversationEnums.LobbyState.LOBBY_STATE_MODERATORS_ONLY &&
  1635. !ConversationUtils.canModerate(currentConversation!!, spreedCapabilities) &&
  1636. !participantPermissions.canIgnoreLobby()
  1637. }
  1638. return false
  1639. }
  1640. private fun disableCallButtons() {
  1641. if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
  1642. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1643. conversationVoiceCallMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1644. conversationVideoMenuItem?.icon?.alpha = SEMI_TRANSPARENT_INT
  1645. conversationVoiceCallMenuItem?.isEnabled = false
  1646. conversationVideoMenuItem?.isEnabled = false
  1647. } else {
  1648. Log.e(TAG, "call buttons were null when trying to disable them")
  1649. }
  1650. }
  1651. }
  1652. private fun enableCallButtons() {
  1653. if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
  1654. if (conversationVoiceCallMenuItem != null && conversationVideoMenuItem != null) {
  1655. conversationVoiceCallMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1656. conversationVideoMenuItem?.icon?.alpha = FULLY_OPAQUE_INT
  1657. conversationVoiceCallMenuItem?.isEnabled = true
  1658. conversationVideoMenuItem?.isEnabled = true
  1659. } else {
  1660. Log.e(TAG, "call buttons were null when trying to enable them")
  1661. }
  1662. }
  1663. }
  1664. private fun isReadOnlyConversation(): Boolean =
  1665. currentConversation?.conversationReadOnlyState != null &&
  1666. currentConversation?.conversationReadOnlyState ==
  1667. ConversationEnums.ConversationReadOnlyState.CONVERSATION_READ_ONLY
  1668. private fun checkLobbyState() {
  1669. if (currentConversation != null &&
  1670. ConversationUtils.isLobbyViewApplicable(currentConversation!!, spreedCapabilities)
  1671. ) {
  1672. if (shouldShowLobby()) {
  1673. binding.lobby.lobbyView.visibility = View.VISIBLE
  1674. binding.messagesListView.visibility = View.GONE
  1675. binding.fragmentContainerActivityChat.visibility = View.GONE
  1676. binding.progressBar.visibility = View.GONE
  1677. val sb = StringBuilder()
  1678. sb.append(resources!!.getText(R.string.nc_lobby_waiting))
  1679. .append("\n\n")
  1680. if (currentConversation?.lobbyTimer != null &&
  1681. currentConversation?.lobbyTimer !=
  1682. 0L
  1683. ) {
  1684. val timestampMS = (currentConversation?.lobbyTimer ?: 0) * DateConstants.SECOND_DIVIDER
  1685. val stringWithStartDate = String.format(
  1686. resources!!.getString(R.string.nc_lobby_start_date),
  1687. dateUtils.getLocalDateTimeStringFromTimestamp(timestampMS)
  1688. )
  1689. val relativeTime = dateUtils.relativeStartTimeForLobby(timestampMS, resources!!)
  1690. sb.append("$stringWithStartDate - $relativeTime")
  1691. .append("\n\n")
  1692. }
  1693. sb.append(currentConversation!!.description)
  1694. binding.lobby.lobbyTextView.text = sb.toString()
  1695. } else {
  1696. binding.lobby.lobbyView.visibility = View.GONE
  1697. binding.messagesListView.visibility = View.VISIBLE
  1698. checkShowMessageInputView()
  1699. }
  1700. } else {
  1701. binding.lobby.lobbyView.visibility = View.GONE
  1702. binding.messagesListView.visibility = View.VISIBLE
  1703. checkShowMessageInputView()
  1704. }
  1705. }
  1706. private fun onRemoteFileBrowsingResult(intent: Intent?) {
  1707. val pathList = intent?.getStringArrayListExtra(RemoteFileBrowserActivity.EXTRA_SELECTED_PATHS)
  1708. if (pathList?.size!! >= 1) {
  1709. pathList
  1710. .chunked(CHUNK_SIZE)
  1711. .forEach { paths ->
  1712. val data = Data.Builder()
  1713. .putLong(KEY_INTERNAL_USER_ID, conversationUser!!.id!!)
  1714. .putString(KEY_ROOM_TOKEN, roomToken)
  1715. .putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
  1716. .build()
  1717. val worker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
  1718. .setInputData(data)
  1719. .build()
  1720. WorkManager.getInstance().enqueue(worker)
  1721. }
  1722. }
  1723. }
  1724. @Throws(IllegalStateException::class)
  1725. private fun onChooseFileResult(intent: Intent?) {
  1726. try {
  1727. checkNotNull(intent)
  1728. filesToUpload.clear()
  1729. intent.clipData?.let {
  1730. for (index in 0 until it.itemCount) {
  1731. filesToUpload.add(it.getItemAt(index).uri.toString())
  1732. }
  1733. } ?: run {
  1734. checkNotNull(intent.data)
  1735. intent.data.let {
  1736. filesToUpload.add(intent.data.toString())
  1737. }
  1738. }
  1739. require(filesToUpload.isNotEmpty())
  1740. val filenamesWithLineBreaks = StringBuilder("\n")
  1741. for (file in filesToUpload) {
  1742. val filename = FileUtils.getFileName(Uri.parse(file), context)
  1743. filenamesWithLineBreaks.append(filename).append("\n")
  1744. }
  1745. val newFragment = FileAttachmentPreviewFragment.newInstance(
  1746. filenamesWithLineBreaks.toString(),
  1747. filesToUpload
  1748. )
  1749. newFragment.setListener { files, caption ->
  1750. uploadFiles(files, caption)
  1751. }
  1752. newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG)
  1753. } catch (e: IllegalStateException) {
  1754. context.resources?.getString(R.string.nc_upload_failed)?.let {
  1755. Snackbar.make(
  1756. binding.root,
  1757. it,
  1758. Snackbar.LENGTH_LONG
  1759. ).show()
  1760. }
  1761. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1762. } catch (e: IllegalArgumentException) {
  1763. context.resources?.getString(R.string.nc_upload_failed)?.let {
  1764. Snackbar.make(
  1765. binding.root,
  1766. it,
  1767. Snackbar.LENGTH_LONG
  1768. ).show()
  1769. }
  1770. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1771. }
  1772. }
  1773. private fun onSelectContactResult(intent: Intent?) {
  1774. val contactUri = intent?.data ?: return
  1775. val cursor: Cursor? = contentResolver!!.query(contactUri, null, null, null, null)
  1776. if (cursor != null && cursor.moveToFirst()) {
  1777. val id = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts._ID))
  1778. val fileName = ContactUtils.getDisplayNameFromDeviceContact(context, id) + ".vcf"
  1779. val file = File(context.cacheDir, fileName)
  1780. writeContactToVcfFile(cursor, file)
  1781. val shareUri = FileProvider.getUriForFile(
  1782. this,
  1783. BuildConfig.APPLICATION_ID,
  1784. File(file.absolutePath)
  1785. )
  1786. uploadFile(shareUri.toString(), false)
  1787. }
  1788. cursor?.close()
  1789. }
  1790. @Throws(IllegalStateException::class)
  1791. private fun onPickCameraResult(intent: Intent?) {
  1792. try {
  1793. filesToUpload.clear()
  1794. if (intent != null && intent.data != null) {
  1795. run {
  1796. intent.data.let {
  1797. filesToUpload.add(intent.data.toString())
  1798. }
  1799. }
  1800. require(filesToUpload.isNotEmpty())
  1801. } else if (videoURI != null) {
  1802. filesToUpload.add(videoURI.toString())
  1803. videoURI = null
  1804. } else {
  1805. error("Failed to get data from intent and uri")
  1806. }
  1807. if (permissionUtil.isFilesPermissionGranted()) {
  1808. val filenamesWithLineBreaks = StringBuilder("\n")
  1809. for (file in filesToUpload) {
  1810. val filename = FileUtils.getFileName(Uri.parse(file), context)
  1811. filenamesWithLineBreaks.append(filename).append("\n")
  1812. }
  1813. val newFragment = FileAttachmentPreviewFragment.newInstance(
  1814. filenamesWithLineBreaks.toString(),
  1815. filesToUpload
  1816. )
  1817. newFragment.setListener { files, caption -> uploadFiles(files, caption) }
  1818. newFragment.show(supportFragmentManager, FileAttachmentPreviewFragment.TAG)
  1819. } else {
  1820. UploadAndShareFilesWorker.requestStoragePermission(this)
  1821. }
  1822. } catch (e: IllegalStateException) {
  1823. Snackbar.make(
  1824. binding.root,
  1825. R.string.nc_upload_failed,
  1826. Snackbar.LENGTH_LONG
  1827. )
  1828. .show()
  1829. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1830. } catch (e: IllegalArgumentException) {
  1831. context.resources?.getString(R.string.nc_upload_failed)?.let {
  1832. Snackbar.make(
  1833. binding.root,
  1834. it,
  1835. Snackbar.LENGTH_LONG
  1836. )
  1837. .show()
  1838. }
  1839. Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
  1840. }
  1841. }
  1842. private fun onMessageSearchResult(intent: Intent?) {
  1843. val messageId = intent?.getStringExtra(MessageSearchActivity.RESULT_KEY_MESSAGE_ID)
  1844. messageId?.let { id ->
  1845. scrollToAndCenterMessageWithId(id)
  1846. }
  1847. }
  1848. private fun executeIfResultOk(result: ActivityResult, onResult: (intent: Intent?) -> Unit) {
  1849. if (result.resultCode == Activity.RESULT_OK) {
  1850. onResult(result.data)
  1851. } else {
  1852. Log.e(TAG, "resultCode for received intent was != ok")
  1853. }
  1854. }
  1855. private fun scrollToMessageWithId(messageId: String) {
  1856. val position = adapter?.items?.indexOfFirst {
  1857. it.item is ChatMessage && (it.item as ChatMessage).id == messageId
  1858. }
  1859. if (position != null && position >= 0) {
  1860. binding.messagesListView.scrollToPosition(position)
  1861. } else {
  1862. Log.d(TAG, "message $messageId that should be scrolled to was not found (scrollToMessageWithId)")
  1863. }
  1864. }
  1865. private fun scrollToAndCenterMessageWithId(messageId: String) {
  1866. adapter?.let {
  1867. val position = it.getMessagePositionByIdInReverse(messageId)
  1868. if (position != -1) {
  1869. layoutManager?.scrollToPositionWithOffset(
  1870. position,
  1871. binding.messagesListView.height / 2
  1872. )
  1873. } else {
  1874. Log.d(
  1875. TAG,
  1876. "message $messageId that should be scrolled to was not found " +
  1877. "(scrollToAndCenterMessageWithId)"
  1878. )
  1879. }
  1880. }
  1881. }
  1882. private fun writeContactToVcfFile(cursor: Cursor, file: File) {
  1883. val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
  1884. val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_VCARD_URI, lookupKey)
  1885. val fd: AssetFileDescriptor = contentResolver!!.openAssetFileDescriptor(uri, "r")!!
  1886. fd.use {
  1887. val fis = fd.createInputStream()
  1888. file.createNewFile()
  1889. fis.use { input ->
  1890. file.outputStream().use { output ->
  1891. input.copyTo(output)
  1892. }
  1893. }
  1894. }
  1895. }
  1896. override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
  1897. super.onRequestPermissionsResult(requestCode, permissions, grantResults)
  1898. if (requestCode == UploadAndShareFilesWorker.REQUEST_PERMISSION) {
  1899. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1900. Log.d(TAG, "upload starting after permissions were granted")
  1901. if (filesToUpload.isNotEmpty()) {
  1902. uploadFiles(filesToUpload)
  1903. }
  1904. } else {
  1905. Snackbar
  1906. .make(binding.root, context.getString(R.string.read_storage_no_permission), Snackbar.LENGTH_LONG)
  1907. .show()
  1908. }
  1909. } else if (requestCode == REQUEST_SHARE_FILE_PERMISSION) {
  1910. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1911. showLocalFilePicker()
  1912. } else {
  1913. Snackbar.make(
  1914. binding.root,
  1915. context.getString(R.string.nc_file_storage_permission),
  1916. Snackbar.LENGTH_LONG
  1917. ).show()
  1918. }
  1919. } else if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION) {
  1920. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1921. // do nothing. user will tap on the microphone again if he wants to record audio..
  1922. } else {
  1923. Snackbar.make(
  1924. binding.root,
  1925. context.getString(R.string.nc_voice_message_missing_audio_permission),
  1926. Snackbar.LENGTH_LONG
  1927. ).show()
  1928. }
  1929. } else if (requestCode == REQUEST_READ_CONTACT_PERMISSION) {
  1930. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1931. val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
  1932. startSelectContactForResult.launch(intent)
  1933. } else {
  1934. Snackbar.make(
  1935. binding.root,
  1936. context.getString(R.string.nc_share_contact_permission),
  1937. Snackbar.LENGTH_LONG
  1938. ).show()
  1939. }
  1940. } else if (requestCode == REQUEST_CAMERA_PERMISSION) {
  1941. if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  1942. Snackbar
  1943. .make(binding.root, context.getString(R.string.camera_permission_granted), Snackbar.LENGTH_LONG)
  1944. .show()
  1945. } else {
  1946. Snackbar
  1947. .make(binding.root, context.getString(R.string.take_photo_permission), Snackbar.LENGTH_LONG)
  1948. .show()
  1949. }
  1950. }
  1951. }
  1952. private fun uploadFiles(files: MutableList<String>, caption: String = "") {
  1953. for (i in 0 until files.size) {
  1954. if (i == files.size - 1) {
  1955. uploadFile(files[i], false, caption)
  1956. } else {
  1957. uploadFile(files[i], false)
  1958. }
  1959. }
  1960. }
  1961. private fun uploadFile(fileUri: String, isVoiceMessage: Boolean, caption: String = "", token: String = "") {
  1962. var metaData = ""
  1963. var room = ""
  1964. if (!participantPermissions.hasChatPermission()) {
  1965. Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
  1966. return
  1967. }
  1968. if (isVoiceMessage) {
  1969. metaData = VOICE_MESSAGE_META_DATA
  1970. }
  1971. if (caption != "") {
  1972. metaData = "{\"caption\":\"$caption\"}"
  1973. }
  1974. if (token == "") room = roomToken else room = token
  1975. chatViewModel.uploadFile(fileUri, room, currentConversation?.displayName!!, metaData)
  1976. }
  1977. private fun showLocalFilePicker() {
  1978. val action = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
  1979. type = "*/*"
  1980. addCategory(Intent.CATEGORY_OPENABLE)
  1981. putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
  1982. }
  1983. startChooseFileIntentForResult.launch(
  1984. Intent.createChooser(
  1985. action,
  1986. context.resources?.getString(
  1987. R.string.nc_upload_choose_local_files
  1988. )
  1989. )
  1990. )
  1991. }
  1992. fun sendSelectLocalFileIntent() {
  1993. if (!permissionUtil.isFilesPermissionGranted()) {
  1994. requestReadFilesPermissions()
  1995. } else {
  1996. showLocalFilePicker()
  1997. }
  1998. }
  1999. fun sendChooseContactIntent() {
  2000. requestReadContacts()
  2001. }
  2002. fun showBrowserScreen() {
  2003. val sharingFileBrowserIntent = Intent(this, RemoteFileBrowserActivity::class.java)
  2004. startRemoteFileBrowsingForResult.launch(sharingFileBrowserIntent)
  2005. }
  2006. fun showShareLocationScreen() {
  2007. Log.d(TAG, "showShareLocationScreen")
  2008. val intent = Intent(this, LocationPickerActivity::class.java)
  2009. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2010. intent.putExtra(BundleKeys.KEY_CHAT_API_VERSION, chatApiVersion)
  2011. startActivity(intent)
  2012. }
  2013. private fun showConversationInfoScreen() {
  2014. val bundle = Bundle()
  2015. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  2016. bundle.putBoolean(BundleKeys.KEY_ROOM_ONE_TO_ONE, isOneToOneConversation())
  2017. val intent = Intent(this, ConversationInfoActivity::class.java)
  2018. intent.putExtras(bundle)
  2019. startActivity(intent)
  2020. }
  2021. private fun validSessionId(): Boolean =
  2022. currentConversation != null &&
  2023. sessionIdAfterRoomJoined?.isNotEmpty() == true &&
  2024. sessionIdAfterRoomJoined != "0"
  2025. @Suppress("Detekt.TooGenericExceptionCaught")
  2026. private fun cancelNotificationsForCurrentConversation() {
  2027. if (conversationUser != null) {
  2028. if (!TextUtils.isEmpty(roomToken)) {
  2029. try {
  2030. NotificationUtils.cancelExistingNotificationsForRoom(
  2031. applicationContext,
  2032. conversationUser!!,
  2033. roomToken
  2034. )
  2035. } catch (e: RuntimeException) {
  2036. Log.w(TAG, "Cancel notifications for current conversation results with an error.", e)
  2037. }
  2038. }
  2039. }
  2040. }
  2041. override fun onPause() {
  2042. super.onPause()
  2043. logConversationInfos("onPause")
  2044. eventBus.unregister(this)
  2045. webSocketInstance?.getSignalingMessageReceiver()?.removeListener(localParticipantMessageListener)
  2046. webSocketInstance?.getSignalingMessageReceiver()?.removeListener(conversationMessageListener)
  2047. findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  2048. checkingLobbyStatus = false
  2049. if (getRoomInfoTimerHandler != null) {
  2050. getRoomInfoTimerHandler?.removeCallbacksAndMessages(null)
  2051. }
  2052. if (conversationUser != null && isActivityNotChangingConfigurations() && isNotInCall()) {
  2053. ApplicationWideCurrentRoomHolder.getInstance().clear()
  2054. if (validSessionId()) {
  2055. leaveRoom(null)
  2056. } else {
  2057. Log.d(TAG, "not leaving room (validSessionId is false)")
  2058. }
  2059. } else {
  2060. Log.d(TAG, "not leaving room...")
  2061. }
  2062. if (mentionAutocomplete != null && mentionAutocomplete!!.isPopupShowing) {
  2063. mentionAutocomplete?.dismissPopup()
  2064. }
  2065. }
  2066. private fun isActivityNotChangingConfigurations(): Boolean = !isChangingConfigurations
  2067. private fun isNotInCall(): Boolean =
  2068. !ApplicationWideCurrentRoomHolder.getInstance().isInCall &&
  2069. !ApplicationWideCurrentRoomHolder.getInstance().isDialing
  2070. private fun setActionBarTitle() {
  2071. val title = binding.chatToolbar.findViewById<TextView>(R.id.chat_toolbar_title)
  2072. viewThemeUtils.platform.colorTextView(title, ColorRole.ON_SURFACE)
  2073. title.text =
  2074. if (currentConversation?.displayName != null) {
  2075. try {
  2076. EmojiCompat.get().process(currentConversation?.displayName as CharSequence).toString()
  2077. } catch (e: java.lang.IllegalStateException) {
  2078. Log.e(TAG, "setActionBarTitle failed $e")
  2079. currentConversation?.displayName
  2080. }
  2081. } else {
  2082. ""
  2083. }
  2084. if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL) {
  2085. var statusMessage = ""
  2086. if (currentConversation?.statusIcon != null) {
  2087. statusMessage += currentConversation?.statusIcon
  2088. }
  2089. if (currentConversation?.statusMessage != null) {
  2090. statusMessage += currentConversation?.statusMessage
  2091. }
  2092. statusMessageViewContents(statusMessage)
  2093. } else {
  2094. if (currentConversation?.type == ConversationEnums.ConversationType.ROOM_GROUP_CALL ||
  2095. currentConversation?.type == ConversationEnums.ConversationType.ROOM_PUBLIC_CALL
  2096. ) {
  2097. var descriptionMessage = ""
  2098. descriptionMessage += currentConversation?.description
  2099. statusMessageViewContents(descriptionMessage)
  2100. }
  2101. }
  2102. }
  2103. private fun statusMessageViewContents(statusMessageContent: String) {
  2104. val statusMessageView = binding.chatToolbar.findViewById<TextView>(R.id.chat_toolbar_status_message)
  2105. if (statusMessageContent.isNotEmpty()) {
  2106. viewThemeUtils.platform.colorTextView(statusMessageView, ColorRole.ON_SURFACE)
  2107. statusMessageView.text = statusMessageContent
  2108. statusMessageView.visibility = View.VISIBLE
  2109. } else {
  2110. statusMessageView.visibility = View.GONE
  2111. }
  2112. }
  2113. public override fun onDestroy() {
  2114. super.onDestroy()
  2115. logConversationInfos("onDestroy")
  2116. findViewById<View>(R.id.toolbar)?.setOnClickListener(null)
  2117. if (actionBar != null) {
  2118. actionBar?.setIcon(null)
  2119. }
  2120. currentlyPlayedVoiceMessage?.let { stopMediaPlayer(it) } // FIXME, mediaplayer can sometimes be null here
  2121. adapter = null
  2122. Log.d(TAG, "inConversation was set to false!")
  2123. }
  2124. private fun joinRoomWithPassword() {
  2125. // if ApplicationWideCurrentRoomHolder contains a session (because a call is active), then keep the sessionId
  2126. if (ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken ==
  2127. currentConversation!!.token
  2128. ) {
  2129. sessionIdAfterRoomJoined = ApplicationWideCurrentRoomHolder.getInstance().session
  2130. // ApplicationWideCurrentRoomHolder.getInstance().currentRoomId = roomId
  2131. ApplicationWideCurrentRoomHolder.getInstance().currentRoomToken = roomToken
  2132. ApplicationWideCurrentRoomHolder.getInstance().userInRoom = conversationUser
  2133. }
  2134. if (!validSessionId()) {
  2135. Log.d(TAG, "sessionID was not valid -> joinRoom")
  2136. val startNanoTime = System.nanoTime()
  2137. Log.d(TAG, "joinRoomWithPassword - joinRoom - calling: $startNanoTime")
  2138. chatViewModel.joinRoom(conversationUser!!, roomToken, roomPassword)
  2139. } else {
  2140. Log.d(TAG, "sessionID was valid -> skip joinRoom")
  2141. setupWebsocket()
  2142. if (webSocketInstance != null) {
  2143. webSocketInstance?.joinRoomWithRoomTokenAndSession(
  2144. roomToken,
  2145. sessionIdAfterRoomJoined,
  2146. externalSignalingServer?.federation
  2147. )
  2148. }
  2149. }
  2150. }
  2151. fun leaveRoom(funToCallWhenLeaveSuccessful: (() -> Unit)?) {
  2152. logConversationInfos("leaveRoom")
  2153. var apiVersion = 1
  2154. // FIXME Fix API checking with guests?
  2155. if (conversationUser != null) {
  2156. apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
  2157. }
  2158. val startNanoTime = System.nanoTime()
  2159. Log.d(TAG, "leaveRoom - leaveRoom - calling: $startNanoTime")
  2160. chatViewModel.leaveRoom(
  2161. credentials!!,
  2162. ApiUtils.getUrlForParticipantsActive(
  2163. apiVersion,
  2164. conversationUser?.baseUrl!!,
  2165. roomToken
  2166. ),
  2167. funToCallWhenLeaveSuccessful
  2168. )
  2169. }
  2170. private fun setupWebsocket() {
  2171. if (currentConversation == null || conversationUser == null) {
  2172. return
  2173. }
  2174. if (currentConversation!!.remoteServer != null) {
  2175. val apiVersion = ApiUtils.getSignalingApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V3, 2, 1))
  2176. ncApi.getSignalingSettings(
  2177. credentials,
  2178. ApiUtils.getUrlForSignalingSettings(apiVersion, conversationUser!!.baseUrl, roomToken)
  2179. ).blockingSubscribe(object : Observer<SignalingSettingsOverall> {
  2180. override fun onSubscribe(d: Disposable) {
  2181. // unused atm
  2182. }
  2183. override fun onNext(signalingSettingsOverall: SignalingSettingsOverall) {
  2184. if (signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer == null ||
  2185. signalingSettingsOverall.ocs!!.settings!!.externalSignalingServer?.isEmpty() == true
  2186. ) {
  2187. return
  2188. }
  2189. externalSignalingServer = ExternalSignalingServer()
  2190. externalSignalingServer!!.externalSignalingServer = signalingSettingsOverall.ocs!!.settings!!
  2191. .externalSignalingServer
  2192. externalSignalingServer!!.externalSignalingTicket = signalingSettingsOverall.ocs!!.settings!!
  2193. .externalSignalingTicket
  2194. externalSignalingServer!!.federation = signalingSettingsOverall.ocs!!.settings!!.federation
  2195. webSocketInstance = WebSocketConnectionHelper.getExternalSignalingInstanceForServer(
  2196. externalSignalingServer!!.externalSignalingServer,
  2197. conversationUser,
  2198. externalSignalingServer!!.externalSignalingTicket,
  2199. TextUtils.isEmpty(credentials)
  2200. )
  2201. }
  2202. override fun onError(e: Throwable) {
  2203. Log.e(CallActivity.TAG, e.message, e)
  2204. }
  2205. override fun onComplete() {
  2206. // unused atm
  2207. }
  2208. })
  2209. } else {
  2210. webSocketInstance = WebSocketConnectionHelper.getWebSocketInstanceForUser(conversationUser!!)
  2211. }
  2212. if (webSocketInstance == null) {
  2213. Log.d(TAG, "webSocketInstance not set up. This should only happen when not using the HPB")
  2214. }
  2215. signalingMessageSender = webSocketInstance?.signalingMessageSender
  2216. webSocketInstance?.getSignalingMessageReceiver()?.addListener(localParticipantMessageListener)
  2217. webSocketInstance?.getSignalingMessageReceiver()?.addListener(conversationMessageListener)
  2218. }
  2219. private fun processCallStartedMessages(chatMessageList: List<ChatMessage>) {
  2220. try {
  2221. val mostRecentCallSystemMessage = adapter?.items?.first {
  2222. it.item is ChatMessage &&
  2223. (it.item as ChatMessage).systemMessageType in
  2224. listOf(
  2225. ChatMessage.SystemMessageType.CALL_STARTED,
  2226. ChatMessage.SystemMessageType.CALL_JOINED,
  2227. ChatMessage.SystemMessageType.CALL_LEFT,
  2228. ChatMessage.SystemMessageType.CALL_ENDED,
  2229. ChatMessage.SystemMessageType.CALL_TRIED,
  2230. ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE,
  2231. ChatMessage.SystemMessageType.CALL_MISSED
  2232. )
  2233. }?.item
  2234. if (mostRecentCallSystemMessage != null) {
  2235. processMostRecentMessage(
  2236. mostRecentCallSystemMessage as ChatMessage,
  2237. chatMessageList
  2238. )
  2239. }
  2240. } catch (e: NoSuchElementException) {
  2241. Log.d(TAG, "No System messages found $e")
  2242. }
  2243. }
  2244. private fun processExpiredMessages() {
  2245. @SuppressLint("NotifyDataSetChanged")
  2246. fun deleteExpiredMessages() {
  2247. val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
  2248. val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
  2249. if (adapter?.items != null) {
  2250. for (itemWrapper in adapter?.items!!) {
  2251. if (itemWrapper.item is ChatMessage) {
  2252. val chatMessage = itemWrapper.item as ChatMessage
  2253. if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) {
  2254. messagesToDelete.add(chatMessage)
  2255. }
  2256. }
  2257. }
  2258. adapter!!.delete(messagesToDelete)
  2259. adapter!!.notifyDataSetChanged()
  2260. }
  2261. }
  2262. if (this::spreedCapabilities.isInitialized) {
  2263. if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.MESSAGE_EXPIRATION)) {
  2264. deleteExpiredMessages()
  2265. }
  2266. } else {
  2267. Log.w(TAG, "spreedCapabilities are not initialized in processExpiredMessages()")
  2268. }
  2269. }
  2270. private fun updateReadStatusOfAllMessages(xChatLastCommonRead: Int?) {
  2271. if (adapter != null) {
  2272. for (message in adapter!!.items) {
  2273. xChatLastCommonRead?.let {
  2274. updateReadStatusOfMessage(message, it)
  2275. }
  2276. }
  2277. adapter!!.notifyDataSetChanged()
  2278. }
  2279. }
  2280. private fun updateReadStatusOfMessage(
  2281. message: MessagesListAdapter<IMessage>.Wrapper<Any>,
  2282. xChatLastCommonRead: Int
  2283. ) {
  2284. if (message.item is ChatMessage) {
  2285. val chatMessage = message.item as ChatMessage
  2286. if (chatMessage.jsonMessageId <= xChatLastCommonRead) {
  2287. chatMessage.readStatus = ReadStatus.READ
  2288. } else {
  2289. chatMessage.readStatus = ReadStatus.SENT
  2290. }
  2291. }
  2292. }
  2293. private fun processMessagesFromTheFuture(chatMessageList: List<ChatMessage>, setUnreadMessagesMarker: Boolean) {
  2294. binding.scrollDownButton.visibility = View.GONE
  2295. val scrollToBottom: Boolean
  2296. if (setUnreadMessagesMarker) {
  2297. scrollToBottom = false
  2298. setUnreadMessageMarker(chatMessageList)
  2299. } else {
  2300. if (isScrolledToBottom()) {
  2301. scrollToBottom = true
  2302. } else {
  2303. scrollToBottom = false
  2304. binding.unreadMessagesPopup.visibility = View.VISIBLE
  2305. // here we have the problem that the chat jumps for every update
  2306. }
  2307. }
  2308. for (chatMessage in chatMessageList) {
  2309. chatMessage.activeUser = conversationUser
  2310. adapter?.let {
  2311. val previousChatMessage = adapter!!.items[1].item
  2312. if (previousChatMessage is ChatMessage) {
  2313. chatMessage.isGrouped = groupMessages(chatMessage, previousChatMessage)
  2314. }
  2315. chatMessage.isOneToOneConversation =
  2316. (currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL)
  2317. chatMessage.isFormerOneToOneConversation =
  2318. (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE)
  2319. Log.d(TAG, "chatMessage to add:" + chatMessage.message)
  2320. it.addToStart(chatMessage, scrollToBottom)
  2321. }
  2322. }
  2323. // workaround to jump back to unread messages marker
  2324. if (setUnreadMessagesMarker) {
  2325. scrollToFirstUnreadMessage()
  2326. }
  2327. }
  2328. private fun isScrolledToBottom() = layoutManager?.findFirstVisibleItemPosition() == 0
  2329. private fun setUnreadMessageMarker(chatMessageList: List<ChatMessage>) {
  2330. val unreadChatMessage = ChatMessage()
  2331. unreadChatMessage.jsonMessageId = UNREAD_MESSAGES_MARKER_ID
  2332. unreadChatMessage.actorId = "-1"
  2333. unreadChatMessage.timestamp = chatMessageList[0].timestamp
  2334. unreadChatMessage.message = context.getString(R.string.nc_new_messages)
  2335. adapter?.addToStart(unreadChatMessage, false)
  2336. }
  2337. private fun processMessagesNotFromTheFuture(chatMessageList: List<ChatMessage>) {
  2338. for (i in chatMessageList.indices) {
  2339. if (chatMessageList.size > i + 1) {
  2340. chatMessageList[i].isGrouped = groupMessages(chatMessageList[i], chatMessageList[i + 1])
  2341. }
  2342. val chatMessage = chatMessageList[i]
  2343. chatMessage.isOneToOneConversation =
  2344. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2345. chatMessage.isFormerOneToOneConversation =
  2346. (currentConversation?.type == ConversationEnums.ConversationType.FORMER_ONE_TO_ONE)
  2347. chatMessage.activeUser = conversationUser
  2348. chatMessage.token = roomToken
  2349. }
  2350. if (adapter != null) {
  2351. adapter?.addToEnd(chatMessageList, false)
  2352. }
  2353. scrollToRequestedMessageIfNeeded()
  2354. // FENOM: add here audio resume policy
  2355. resumeAudioPlaybackIfNeeded()
  2356. }
  2357. private fun scrollToFirstUnreadMessage() {
  2358. adapter?.let {
  2359. scrollToAndCenterMessageWithId(UNREAD_MESSAGES_MARKER_ID.toString())
  2360. }
  2361. }
  2362. private fun groupMessages(message1: ChatMessage, message2: ChatMessage): Boolean {
  2363. val message1IsSystem = message1.systemMessage.isNotEmpty()
  2364. val message2IsSystem = message2.systemMessage.isNotEmpty()
  2365. if (message1IsSystem != message2IsSystem) {
  2366. return false
  2367. }
  2368. if (message1.actorType == "bots" && message1.actorId != "changelog") {
  2369. return false
  2370. }
  2371. if (!message1IsSystem &&
  2372. (
  2373. (message1.actorType != message2.actorType) ||
  2374. (message2.actorId != message1.actorId)
  2375. )
  2376. ) {
  2377. return false
  2378. }
  2379. val timeDifference = dateUtils.getTimeDifferenceInSeconds(
  2380. message2.timestamp,
  2381. message1.timestamp
  2382. )
  2383. val isLessThan5Min = timeDifference > FIVE_MINUTES_IN_SECONDS
  2384. if (isSameDayMessages(message2, message1) &&
  2385. (message2.actorId == message1.actorId) &&
  2386. (!isLessThan5Min) &&
  2387. (message2.lastEditTimestamp == 0L || message1.lastEditTimestamp == 0L)
  2388. ) {
  2389. return true
  2390. }
  2391. return false
  2392. }
  2393. private fun determinePreviousMessageIds(chatMessageList: List<ChatMessage>) {
  2394. var previousMessageId = NO_PREVIOUS_MESSAGE_ID
  2395. for (i in chatMessageList.indices.reversed()) {
  2396. val chatMessage = chatMessageList[i]
  2397. if (previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2398. chatMessage.previousMessageId = previousMessageId
  2399. } else {
  2400. adapter?.let {
  2401. if (!it.isEmpty) {
  2402. if (it.items[0].item is ChatMessage) {
  2403. chatMessage.previousMessageId = (it.items[0].item as ChatMessage).jsonMessageId
  2404. } else if (it.items.size > 1 && it.items[1].item is ChatMessage) {
  2405. chatMessage.previousMessageId = (it.items[1].item as ChatMessage).jsonMessageId
  2406. }
  2407. }
  2408. }
  2409. }
  2410. previousMessageId = chatMessage.jsonMessageId
  2411. }
  2412. }
  2413. /**
  2414. * this method must be called after that the adapter has finished loading ChatMessages items
  2415. * it searches by ID the message that was playing,s
  2416. * then, if it finds it, it restores audio position
  2417. * and eventually resumes audio playback
  2418. * @author Giacomo Pacini
  2419. */
  2420. private fun resumeAudioPlaybackIfNeeded() {
  2421. if (voiceMessageToRestoreId != "") {
  2422. Log.d(RESUME_AUDIO_TAG, "begin method to resume audio playback")
  2423. val pair = getItemFromAdapter(voiceMessageToRestoreId)
  2424. currentlyPlayedVoiceMessage = pair?.first
  2425. val voiceMessagePosition = pair?.second!!
  2426. lastRecordMediaPosition = voiceMessageToRestoreAudioPosition * 1000
  2427. Log.d(RESUME_AUDIO_TAG, "trying to resume audio")
  2428. binding.messagesListView.scrollToPosition(voiceMessagePosition)
  2429. // WORKAROUND TO FETCH FILE INFO:
  2430. currentlyPlayedVoiceMessage!!.getImageUrl()
  2431. // see getImageUrl() source code
  2432. setUpWaveform(currentlyPlayedVoiceMessage!!, voiceMessageToRestoreWasPlaying)
  2433. Log.d(RESUME_AUDIO_TAG, "resume audio procedure completed")
  2434. } else {
  2435. Log.d(RESUME_AUDIO_TAG, "No voice message to restore")
  2436. }
  2437. voiceMessageToRestoreId = ""
  2438. voiceMessageToRestoreAudioPosition = 0
  2439. voiceMessageToRestoreWasPlaying = false
  2440. }
  2441. private fun getItemFromAdapter(messageId: String): Pair<ChatMessage, Int>? {
  2442. if (adapter != null) {
  2443. val messagePosition = adapter!!.items!!.indexOfFirst {
  2444. it.item is ChatMessage && (it.item as ChatMessage).id == messageId
  2445. }
  2446. if (messagePosition >= 0) {
  2447. val currentItem = adapter?.items?.get(messagePosition)?.item
  2448. if (currentItem is ChatMessage && currentItem.id == messageId) {
  2449. return Pair(currentItem, messagePosition)
  2450. } else {
  2451. Log.d(TAG, "currentItem retrieved was not chatmessage or its id was not correct")
  2452. }
  2453. } else {
  2454. Log.d(TAG, "messagePosition is -1, adapter # of items: " + adapter!!.itemCount)
  2455. }
  2456. } else {
  2457. Log.d(TAG, "TalkMessagesListAdapter is null")
  2458. }
  2459. return null
  2460. }
  2461. private fun scrollToRequestedMessageIfNeeded() {
  2462. intent.getStringExtra(BundleKeys.KEY_MESSAGE_ID)?.let {
  2463. scrollToMessageWithId(it)
  2464. }
  2465. }
  2466. private fun isSameDayNonSystemMessages(messageLeft: ChatMessage, messageRight: ChatMessage): Boolean =
  2467. TextUtils.isEmpty(messageLeft.systemMessage) &&
  2468. TextUtils.isEmpty(messageRight.systemMessage) &&
  2469. DateFormatter.isSameDay(messageLeft.createdAt, messageRight.createdAt)
  2470. private fun isSameDayMessages(message1: ChatMessage, message2: ChatMessage): Boolean =
  2471. DateFormatter.isSameDay(message1.createdAt, message2.createdAt)
  2472. override fun onLoadMore(page: Int, totalItemsCount: Int) {
  2473. val id = (
  2474. adapter?.items?.last {
  2475. it.item is ChatMessage
  2476. }?.item as ChatMessage
  2477. ).jsonMessageId
  2478. val urlForChatting = ApiUtils.getUrlForChat(chatApiVersion, conversationUser?.baseUrl, roomToken)
  2479. chatViewModel.loadMoreMessages(
  2480. beforeMessageId = id.toLong(),
  2481. withUrl = urlForChatting,
  2482. withCredentials = credentials!!,
  2483. withMessageLimit = MESSAGE_PULL_LIMIT,
  2484. roomToken = currentConversation!!.token
  2485. )
  2486. }
  2487. override fun format(date: Date): String =
  2488. if (DateFormatter.isToday(date)) {
  2489. resources!!.getString(R.string.nc_date_header_today)
  2490. } else if (DateFormatter.isYesterday(date)) {
  2491. resources!!.getString(R.string.nc_date_header_yesterday)
  2492. } else {
  2493. DateFormatter.format(date, DateFormatter.Template.STRING_DAY_MONTH_YEAR)
  2494. }
  2495. override fun onCreateOptionsMenu(menu: Menu): Boolean {
  2496. super.onCreateOptionsMenu(menu)
  2497. menuInflater.inflate(R.menu.menu_conversation, menu)
  2498. if (conversationUser?.userId == "?") {
  2499. menu.removeItem(R.id.conversation_info)
  2500. } else {
  2501. loadAvatarForStatusBar()
  2502. setActionBarTitle()
  2503. }
  2504. return true
  2505. }
  2506. override fun onPrepareOptionsMenu(menu: Menu): Boolean {
  2507. super.onPrepareOptionsMenu(menu)
  2508. if (this::spreedCapabilities.isInitialized) {
  2509. if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.READ_ONLY_ROOMS)) {
  2510. checkShowCallButtons()
  2511. }
  2512. val searchItem = menu.findItem(R.id.conversation_search)
  2513. searchItem.isVisible = CapabilitiesUtil.isUnifiedSearchAvailable(spreedCapabilities) &&
  2514. currentConversation!!.remoteServer.isNullOrEmpty()
  2515. if (currentConversation!!.remoteServer != null ||
  2516. !CapabilitiesUtil.isSharedItemsAvailable(spreedCapabilities)
  2517. ) {
  2518. menu.removeItem(R.id.shared_items)
  2519. }
  2520. if (CapabilitiesUtil.isAbleToCall(spreedCapabilities)) {
  2521. conversationVoiceCallMenuItem = menu.findItem(R.id.conversation_voice_call)
  2522. conversationVideoMenuItem = menu.findItem(R.id.conversation_video_call)
  2523. if (CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.SILENT_CALL)) {
  2524. Handler().post {
  2525. findViewById<View?>(R.id.conversation_voice_call)?.setOnLongClickListener {
  2526. showCallButtonMenu(true)
  2527. true
  2528. }
  2529. }
  2530. Handler().post {
  2531. findViewById<View?>(R.id.conversation_video_call)?.setOnLongClickListener {
  2532. showCallButtonMenu(false)
  2533. true
  2534. }
  2535. }
  2536. }
  2537. } else {
  2538. menu.removeItem(R.id.conversation_video_call)
  2539. menu.removeItem(R.id.conversation_voice_call)
  2540. }
  2541. }
  2542. return true
  2543. }
  2544. override fun onOptionsItemSelected(item: MenuItem): Boolean =
  2545. when (item.itemId) {
  2546. R.id.conversation_video_call -> {
  2547. startACall(false, false)
  2548. true
  2549. }
  2550. R.id.conversation_voice_call -> {
  2551. startACall(true, false)
  2552. true
  2553. }
  2554. R.id.conversation_info -> {
  2555. showConversationInfoScreen()
  2556. true
  2557. }
  2558. R.id.shared_items -> {
  2559. showSharedItems()
  2560. true
  2561. }
  2562. R.id.conversation_search -> {
  2563. startMessageSearch()
  2564. true
  2565. }
  2566. else -> super.onOptionsItemSelected(item)
  2567. }
  2568. private fun showSharedItems() {
  2569. val intent = Intent(this, SharedItemsActivity::class.java)
  2570. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2571. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2572. intent.putExtra(
  2573. SharedItemsActivity.KEY_USER_IS_OWNER_OR_MODERATOR,
  2574. ConversationUtils.isParticipantOwnerOrModerator(currentConversation!!)
  2575. )
  2576. startActivity(intent)
  2577. }
  2578. private fun startMessageSearch() {
  2579. val intent = Intent(this, MessageSearchActivity::class.java)
  2580. intent.putExtra(KEY_CONVERSATION_NAME, currentConversation?.displayName)
  2581. intent.putExtra(KEY_ROOM_TOKEN, roomToken)
  2582. startMessageSearchForResult.launch(intent)
  2583. }
  2584. private fun handleSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  2585. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  2586. val chatMessageIterator = chatMessageMap.iterator()
  2587. while (chatMessageIterator.hasNext()) {
  2588. val currentMessage = chatMessageIterator.next()
  2589. if (isInfoMessageAboutDeletion(currentMessage) ||
  2590. isReactionsMessage(currentMessage) ||
  2591. isPollVotedMessage(currentMessage) ||
  2592. isEditMessage(currentMessage)
  2593. ) {
  2594. chatMessageIterator.remove()
  2595. }
  2596. }
  2597. return chatMessageMap.values.toList()
  2598. }
  2599. private fun handleExpandableSystemMessages(chatMessageList: List<ChatMessage>): List<ChatMessage> {
  2600. val chatMessageMap = chatMessageList.map { it.id to it }.toMap().toMutableMap()
  2601. val chatMessageIterator = chatMessageMap.iterator()
  2602. while (chatMessageIterator.hasNext()) {
  2603. val currentMessage = chatMessageIterator.next()
  2604. val previousMessage = chatMessageMap[currentMessage.value.previousMessageId.toString()]
  2605. if (isSystemMessage(currentMessage.value) &&
  2606. previousMessage?.systemMessageType == currentMessage.value.systemMessageType
  2607. ) {
  2608. previousMessage?.expandableParent = true
  2609. currentMessage.value.expandableParent = false
  2610. if (currentMessage.value.lastItemOfExpandableGroup == 0) {
  2611. currentMessage.value.lastItemOfExpandableGroup = currentMessage.value.jsonMessageId
  2612. }
  2613. previousMessage?.lastItemOfExpandableGroup = currentMessage.value.lastItemOfExpandableGroup
  2614. previousMessage?.expandableChildrenAmount = currentMessage.value.expandableChildrenAmount + 1
  2615. }
  2616. }
  2617. return chatMessageMap.values.toList()
  2618. }
  2619. private fun isInfoMessageAboutDeletion(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
  2620. currentMessage.value.parentMessageId != null &&
  2621. currentMessage.value.systemMessageType == ChatMessage
  2622. .SystemMessageType.MESSAGE_DELETED
  2623. private fun isReactionsMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
  2624. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION ||
  2625. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_DELETED ||
  2626. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.REACTION_REVOKED
  2627. private fun isEditMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
  2628. currentMessage.value.parentMessageId != null &&
  2629. currentMessage.value.systemMessageType == ChatMessage
  2630. .SystemMessageType.MESSAGE_EDITED
  2631. private fun isPollVotedMessage(currentMessage: MutableMap.MutableEntry<String, ChatMessage>): Boolean =
  2632. currentMessage.value.systemMessageType == ChatMessage.SystemMessageType.POLL_VOTED
  2633. private fun startACall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean) {
  2634. currentConversation?.let {
  2635. if (conversationUser != null) {
  2636. val pp = ParticipantPermissions(spreedCapabilities, it)
  2637. if (!pp.canStartCall() && currentConversation?.hasCall == false) {
  2638. Snackbar.make(binding.root, R.string.startCallForbidden, Snackbar.LENGTH_LONG).show()
  2639. } else {
  2640. ApplicationWideCurrentRoomHolder.getInstance().isDialing = true
  2641. val callIntent = getIntentForCall(isVoiceOnlyCall, callWithoutNotification)
  2642. if (callIntent != null) {
  2643. startActivity(callIntent)
  2644. }
  2645. }
  2646. }
  2647. }
  2648. }
  2649. private fun getIntentForCall(isVoiceOnlyCall: Boolean, callWithoutNotification: Boolean): Intent? {
  2650. currentConversation?.let {
  2651. val bundle = Bundle()
  2652. bundle.putString(KEY_ROOM_TOKEN, roomToken)
  2653. // bundle.putString(KEY_ROOM_ID, roomId)
  2654. bundle.putString(BundleKeys.KEY_CONVERSATION_PASSWORD, roomPassword)
  2655. bundle.putString(BundleKeys.KEY_MODIFIED_BASE_URL, conversationUser?.baseUrl!!)
  2656. bundle.putString(KEY_CONVERSATION_NAME, it.displayName)
  2657. bundle.putInt(KEY_RECORDING_STATE, it.callRecording)
  2658. bundle.putBoolean(KEY_IS_MODERATOR, ConversationUtils.isParticipantOwnerOrModerator(it))
  2659. bundle.putBoolean(
  2660. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_AUDIO,
  2661. participantPermissions.canPublishAudio()
  2662. )
  2663. bundle.putBoolean(
  2664. BundleKeys.KEY_PARTICIPANT_PERMISSION_CAN_PUBLISH_VIDEO,
  2665. participantPermissions.canPublishVideo()
  2666. )
  2667. if (isVoiceOnlyCall) {
  2668. bundle.putBoolean(KEY_CALL_VOICE_ONLY, true)
  2669. }
  2670. if (callWithoutNotification) {
  2671. bundle.putBoolean(BundleKeys.KEY_CALL_WITHOUT_NOTIFICATION, true)
  2672. }
  2673. if (it.objectType == ConversationEnums.ObjectType.ROOM) {
  2674. bundle.putBoolean(KEY_IS_BREAKOUT_ROOM, true)
  2675. }
  2676. val callIntent = Intent(this, CallActivity::class.java)
  2677. callIntent.putExtras(bundle)
  2678. return callIntent
  2679. } ?: run {
  2680. return null
  2681. }
  2682. }
  2683. override fun onClickReaction(chatMessage: ChatMessage, emoji: String) {
  2684. VibrationUtils.vibrateShort(context)
  2685. if (chatMessage.reactionsSelf?.contains(emoji) == true) {
  2686. chatViewModel.deleteReaction(roomToken, chatMessage, emoji)
  2687. } else {
  2688. chatViewModel.addReaction(roomToken, chatMessage, emoji)
  2689. }
  2690. }
  2691. override fun onLongClickReactions(chatMessage: ChatMessage) {
  2692. ShowReactionsDialog(
  2693. this,
  2694. roomToken,
  2695. chatMessage,
  2696. conversationUser,
  2697. participantPermissions.hasChatPermission(),
  2698. ncApi
  2699. ).show()
  2700. }
  2701. override fun onOpenMessageActionsDialog(chatMessage: ChatMessage) {
  2702. openMessageActionsDialog(chatMessage)
  2703. }
  2704. override fun onMessageViewLongClick(view: View?, message: IMessage?) {
  2705. openMessageActionsDialog(message)
  2706. }
  2707. override fun onPreviewMessageLongClick(chatMessage: ChatMessage) {
  2708. onOpenMessageActionsDialog(chatMessage)
  2709. }
  2710. private fun openMessageActionsDialog(iMessage: IMessage?) {
  2711. val message = iMessage as ChatMessage
  2712. if (hasVisibleItems(message) &&
  2713. !isSystemMessage(message) &&
  2714. message.id != "-3"
  2715. ) {
  2716. MessageActionsDialog(
  2717. this,
  2718. message,
  2719. conversationUser,
  2720. currentConversation,
  2721. isShowMessageDeletionButton(message),
  2722. participantPermissions.hasChatPermission(),
  2723. spreedCapabilities
  2724. ).show()
  2725. }
  2726. }
  2727. private fun isSystemMessage(message: ChatMessage): Boolean =
  2728. ChatMessage.MessageType.SYSTEM_MESSAGE == message.getCalculateMessageType()
  2729. fun deleteMessage(message: IMessage) {
  2730. if (!participantPermissions.hasChatPermission()) {
  2731. Log.w(
  2732. TAG,
  2733. "Deletion of message is skipped because of restrictions by permissions. " +
  2734. "This method should not have been called!"
  2735. )
  2736. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  2737. } else {
  2738. var apiVersion = 1
  2739. // FIXME Fix API checking with guests?
  2740. if (conversationUser != null) {
  2741. apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  2742. }
  2743. chatViewModel.deleteChatMessages(
  2744. credentials!!,
  2745. ApiUtils.getUrlForChatMessage(
  2746. apiVersion,
  2747. conversationUser?.baseUrl!!,
  2748. roomToken,
  2749. message.id!!
  2750. ),
  2751. message.id!!
  2752. )
  2753. }
  2754. }
  2755. fun replyPrivately(message: IMessage?) {
  2756. val apiVersion =
  2757. ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
  2758. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  2759. apiVersion,
  2760. conversationUser?.baseUrl!!,
  2761. "1",
  2762. null,
  2763. message?.user?.id?.substring(INVITE_LENGTH),
  2764. null
  2765. )
  2766. chatViewModel.createRoom(
  2767. credentials!!,
  2768. retrofitBucket.url!!,
  2769. retrofitBucket.queryMap!!
  2770. )
  2771. }
  2772. fun forwardMessage(message: IMessage?) {
  2773. val bundle = Bundle()
  2774. bundle.putBoolean(BundleKeys.KEY_FORWARD_MSG_FLAG, true)
  2775. bundle.putString(BundleKeys.KEY_FORWARD_MSG_TEXT, message?.text)
  2776. bundle.putString(BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM, roomToken)
  2777. val intent = Intent(this, ConversationsListActivity::class.java)
  2778. intent.putExtras(bundle)
  2779. startActivity(intent)
  2780. }
  2781. fun remindMeLater(message: ChatMessage?) {
  2782. Log.d(TAG, "remindMeLater called")
  2783. val chatApiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1, 1))
  2784. val newFragment: DialogFragment = DateTimePickerFragment.newInstance(roomToken, message!!.id, chatApiVersion)
  2785. newFragment.show(supportFragmentManager, DateTimePickerFragment.TAG)
  2786. }
  2787. fun markAsUnread(message: IMessage?) {
  2788. val chatMessage = message as ChatMessage?
  2789. if (chatMessage!!.previousMessageId > NO_PREVIOUS_MESSAGE_ID) {
  2790. chatViewModel.setChatReadMarker(
  2791. credentials!!,
  2792. ApiUtils.getUrlForChatReadMarker(
  2793. ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(ApiUtils.API_V1)),
  2794. conversationUser?.baseUrl!!,
  2795. roomToken
  2796. ),
  2797. chatMessage.previousMessageId
  2798. )
  2799. }
  2800. }
  2801. fun copyMessage(message: IMessage?) {
  2802. val clipboardManager =
  2803. getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
  2804. val clipData = ClipData.newPlainText(
  2805. resources?.getString(R.string.nc_app_product_name),
  2806. message?.text
  2807. )
  2808. clipboardManager.setPrimaryClip(clipData)
  2809. }
  2810. fun translateMessage(message: IMessage?) {
  2811. val bundle = Bundle()
  2812. bundle.putString(BundleKeys.KEY_TRANSLATE_MESSAGE, message?.text)
  2813. val intent = Intent(this, TranslateActivity::class.java)
  2814. intent.putExtras(bundle)
  2815. startActivity(intent)
  2816. }
  2817. fun share(message: ChatMessage) {
  2818. val filename = message.selectedIndividualHashMap!!["name"]
  2819. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2820. val shareUri = FileProvider.getUriForFile(
  2821. this,
  2822. BuildConfig.APPLICATION_ID,
  2823. File(path)
  2824. )
  2825. val shareIntent: Intent = Intent().apply {
  2826. action = Intent.ACTION_SEND
  2827. putExtra(Intent.EXTRA_STREAM, shareUri)
  2828. type = Mimetype.IMAGE_PREFIX_GENERIC
  2829. addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
  2830. }
  2831. startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.send_to)))
  2832. }
  2833. fun checkIfSharable(message: ChatMessage) {
  2834. val filename = message.selectedIndividualHashMap!!["name"]
  2835. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2836. val file = File(context.cacheDir, filename!!)
  2837. if (file.exists()) {
  2838. share(message)
  2839. } else {
  2840. downloadFileToCache(message, false) {
  2841. share(message)
  2842. }
  2843. }
  2844. }
  2845. private fun showSaveToStorageWarning(message: ChatMessage) {
  2846. val saveFragment: DialogFragment = SaveToStorageDialogFragment.newInstance(
  2847. message.selectedIndividualHashMap!!["name"]!!
  2848. )
  2849. saveFragment.show(
  2850. supportFragmentManager,
  2851. SaveToStorageDialogFragment.TAG
  2852. )
  2853. }
  2854. fun checkIfSaveable(message: ChatMessage) {
  2855. val filename = message.selectedIndividualHashMap!!["name"]
  2856. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2857. val file = File(context.cacheDir, filename!!)
  2858. if (file.exists()) {
  2859. showSaveToStorageWarning(message)
  2860. } else {
  2861. downloadFileToCache(message, false) {
  2862. showSaveToStorageWarning(message)
  2863. }
  2864. }
  2865. }
  2866. fun shareToNotes(message: ChatMessage, roomToken: String) {
  2867. val apiVersion = ApiUtils.getChatApiVersion(spreedCapabilities, intArrayOf(1))
  2868. val type = message.getCalculateMessageType()
  2869. var shareUri: Uri? = null
  2870. val data: HashMap<String?, String?>?
  2871. var metaData: String = ""
  2872. var objectId: String = ""
  2873. if (message.hasFileAttachment()) {
  2874. val filename = message.selectedIndividualHashMap!!["name"]
  2875. path = applicationContext.cacheDir.absolutePath + "/" + filename
  2876. shareUri = FileProvider.getUriForFile(
  2877. this,
  2878. BuildConfig.APPLICATION_ID,
  2879. File(path)
  2880. )
  2881. this.grantUriPermission(
  2882. applicationContext.packageName,
  2883. shareUri,
  2884. Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
  2885. )
  2886. } else if (message.hasGeoLocation()) {
  2887. data = message.messageParameters?.get("object")
  2888. objectId = data?.get("id")!!
  2889. val name = data["name"]!!
  2890. val lat = data["latitude"]!!
  2891. val lon = data["longitude"]!!
  2892. metaData =
  2893. "{\"type\":\"geo-location\",\"id\":\"geo:$lat,$lon\",\"latitude\":\"$lat\"," +
  2894. "\"longitude\":\"$lon\",\"name\":\"$name\"}"
  2895. }
  2896. when (type) {
  2897. ChatMessage.MessageType.VOICE_MESSAGE -> {
  2898. uploadFile(shareUri.toString(), true, token = roomToken)
  2899. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2900. }
  2901. ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE -> {
  2902. val caption = if (message.message != "{file}") message.message else ""
  2903. if (null != shareUri) {
  2904. try {
  2905. context.contentResolver.openInputStream(shareUri)?.close()
  2906. uploadFile(shareUri.toString(), false, caption!!, roomToken)
  2907. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2908. } catch (e: java.lang.Exception) {
  2909. Log.w(TAG, "File corresponding to the uri does not exist " + shareUri.toString())
  2910. downloadFileToCache(message, false) {
  2911. uploadFile(shareUri.toString(), false, caption!!, roomToken)
  2912. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2913. }
  2914. }
  2915. }
  2916. }
  2917. ChatMessage.MessageType.SINGLE_NC_GEOLOCATION_MESSAGE -> {
  2918. chatViewModel.shareLocationToNotes(
  2919. credentials!!,
  2920. ApiUtils.getUrlToSendLocation(apiVersion, conversationUser!!.baseUrl!!, roomToken),
  2921. "geo-location",
  2922. objectId,
  2923. metaData
  2924. )
  2925. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2926. }
  2927. ChatMessage.MessageType.REGULAR_TEXT_MESSAGE -> {
  2928. chatViewModel.shareToNotes(
  2929. credentials!!,
  2930. ApiUtils.getUrlForChat(apiVersion, conversationUser!!.baseUrl!!, roomToken),
  2931. message.message!!,
  2932. conversationUser!!.displayName!!
  2933. )
  2934. Snackbar.make(binding.root, R.string.nc_message_sent, Snackbar.LENGTH_SHORT).show()
  2935. }
  2936. else -> {}
  2937. }
  2938. }
  2939. fun openInFilesApp(message: ChatMessage) {
  2940. val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
  2941. val link = message.selectedIndividualHashMap!!["link"]
  2942. val fileViewerUtils = FileViewerUtils(this, message.activeUser!!)
  2943. fileViewerUtils.openFileInFilesApp(link!!, keyID!!)
  2944. }
  2945. private fun hasVisibleItems(message: ChatMessage): Boolean =
  2946. !message.isDeleted ||
  2947. // copy message
  2948. message.replyable ||
  2949. // reply to
  2950. message.replyable &&
  2951. // reply privately
  2952. conversationUser?.userId?.isNotEmpty() == true &&
  2953. conversationUser!!.userId != "?" &&
  2954. message.user.id.startsWith("users/") &&
  2955. message.user.id.substring(ACTOR_LENGTH) != currentConversation?.actorId &&
  2956. currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  2957. isShowMessageDeletionButton(message) ||
  2958. // delete
  2959. ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() ||
  2960. // forward
  2961. message.previousMessageId > NO_PREVIOUS_MESSAGE_ID &&
  2962. // mark as unread
  2963. ChatMessage.MessageType.SYSTEM_MESSAGE != message.getCalculateMessageType() &&
  2964. BuildConfig.DEBUG
  2965. private fun setMessageAsDeleted(message: IMessage?) {
  2966. val messageTemp = message as ChatMessage
  2967. messageTemp.isDeleted = true
  2968. messageTemp.message = getString(R.string.message_deleted_by_you)
  2969. messageTemp.isOneToOneConversation =
  2970. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2971. messageTemp.activeUser = conversationUser
  2972. adapter?.update(messageTemp)
  2973. }
  2974. private fun setMessageAsEdited(message: IMessage?, newString: String) {
  2975. val messageTemp = message as ChatMessage
  2976. messageTemp.lastEditTimestamp = message.lastEditTimestamp
  2977. messageTemp.message = newString
  2978. val index = adapter?.getMessagePositionById(messageTemp.id)!!
  2979. if (index > 0) {
  2980. val adapterMsg = adapter?.items?.get(index)?.item as ChatMessage
  2981. messageTemp.parentMessageId = adapterMsg.parentMessageId
  2982. }
  2983. messageTemp.isOneToOneConversation =
  2984. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2985. messageTemp.activeUser = conversationUser
  2986. adapter?.update(messageTemp)
  2987. }
  2988. private fun updateMessageInsideAdapter(message: IMessage?) {
  2989. message?.let {
  2990. val messageTemp = message as ChatMessage
  2991. // TODO is this needed?
  2992. messageTemp.isOneToOneConversation =
  2993. currentConversation?.type == ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL
  2994. messageTemp.activeUser = conversationUser
  2995. adapter?.update(messageTemp)
  2996. }
  2997. }
  2998. fun updateUiToAddReaction(message: ChatMessage, emoji: String) {
  2999. if (message.reactions == null) {
  3000. message.reactions = LinkedHashMap()
  3001. }
  3002. if (message.reactionsSelf == null) {
  3003. message.reactionsSelf = ArrayList()
  3004. }
  3005. var amount = message.reactions!![emoji]
  3006. if (amount == null) {
  3007. amount = 0
  3008. }
  3009. message.reactions!![emoji] = amount + 1
  3010. message.reactionsSelf!!.add(emoji)
  3011. adapter?.update(message)
  3012. }
  3013. fun updateUiToDeleteReaction(message: ChatMessage, emoji: String) {
  3014. if (message.reactions == null) {
  3015. message.reactions = LinkedHashMap()
  3016. }
  3017. if (message.reactionsSelf == null) {
  3018. message.reactionsSelf = ArrayList()
  3019. }
  3020. var amount = message.reactions!![emoji]
  3021. if (amount == null) {
  3022. amount = 0
  3023. }
  3024. message.reactions!![emoji] = amount - 1
  3025. if (message.reactions!![emoji]!! <= 0) {
  3026. message.reactions!!.remove(emoji)
  3027. }
  3028. message.reactionsSelf!!.remove(emoji)
  3029. adapter?.update(message)
  3030. }
  3031. private fun isShowMessageDeletionButton(message: ChatMessage): Boolean {
  3032. val isUserAllowedByPrivileges = userAllowedByPrivilages(message)
  3033. val isOlderThanSixHours = message
  3034. .createdAt
  3035. .before(Date(System.currentTimeMillis() - AGE_THRESHOLD_FOR_DELETE_MESSAGE))
  3036. val hasDeleteMessagesUnlimitedCapability = CapabilitiesUtil.hasSpreedFeatureCapability(
  3037. spreedCapabilities,
  3038. SpreedFeatures.DELETE_MESSAGES_UNLIMITED
  3039. )
  3040. return when {
  3041. !isUserAllowedByPrivileges -> false
  3042. !hasDeleteMessagesUnlimitedCapability && isOlderThanSixHours -> false
  3043. message.systemMessageType != ChatMessage.SystemMessageType.DUMMY -> false
  3044. message.isDeleted -> false
  3045. !CapabilitiesUtil.hasSpreedFeatureCapability(spreedCapabilities, SpreedFeatures.DELETE_MESSAGES) -> false
  3046. !participantPermissions.hasChatPermission() -> false
  3047. hasDeleteMessagesUnlimitedCapability -> true
  3048. else -> true
  3049. }
  3050. }
  3051. fun userAllowedByPrivilages(message: ChatMessage): Boolean {
  3052. if (conversationUser == null) return false
  3053. val isUserAllowedByPrivileges = if (message.actorId == conversationUser!!.userId) {
  3054. true
  3055. } else {
  3056. ConversationUtils.canModerate(currentConversation!!, spreedCapabilities)
  3057. }
  3058. return isUserAllowedByPrivileges
  3059. }
  3060. override fun hasContentFor(message: ChatMessage, type: Byte): Boolean =
  3061. when (type) {
  3062. CONTENT_TYPE_LOCATION -> message.hasGeoLocation()
  3063. CONTENT_TYPE_VOICE_MESSAGE -> message.isVoiceMessage
  3064. CONTENT_TYPE_POLL -> message.isPoll()
  3065. CONTENT_TYPE_LINK_PREVIEW -> message.isLinkPreview()
  3066. CONTENT_TYPE_SYSTEM_MESSAGE -> !TextUtils.isEmpty(message.systemMessage)
  3067. CONTENT_TYPE_UNREAD_NOTICE_MESSAGE -> message.id == UNREAD_MESSAGES_MARKER_ID.toString()
  3068. CONTENT_TYPE_CALL_STARTED -> message.id == "-2"
  3069. CONTENT_TYPE_TEMP -> message.id == "-3"
  3070. CONTENT_TYPE_DECK_CARD -> message.isDeckCard()
  3071. else -> false
  3072. }
  3073. private fun processMostRecentMessage(recent: ChatMessage, chatMessageList: List<ChatMessage>) {
  3074. when (recent.systemMessageType) {
  3075. ChatMessage.SystemMessageType.CALL_STARTED -> { // add CallStartedMessage with id -2
  3076. if (!callStarted) {
  3077. val callStartedChatMessage = ChatMessage()
  3078. callStartedChatMessage.jsonMessageId = CALL_STARTED_ID
  3079. callStartedChatMessage.actorId = "-2"
  3080. val name = if (recent.actorDisplayName.isNullOrEmpty()) "Guest" else recent.actorDisplayName
  3081. callStartedChatMessage.actorDisplayName = name
  3082. callStartedChatMessage.actorType = recent.actorType
  3083. callStartedChatMessage.timestamp = chatMessageList[0].timestamp
  3084. callStartedChatMessage.message = null
  3085. adapter?.addToStart(callStartedChatMessage, false)
  3086. callStarted = true
  3087. }
  3088. } // remove CallStartedMessage with id -2
  3089. ChatMessage.SystemMessageType.CALL_ENDED,
  3090. ChatMessage.SystemMessageType.CALL_MISSED,
  3091. ChatMessage.SystemMessageType.CALL_TRIED,
  3092. ChatMessage.SystemMessageType.CALL_ENDED_EVERYONE -> {
  3093. adapter?.deleteById("-2")
  3094. callStarted = false
  3095. } // remove message of id -2
  3096. else -> {}
  3097. }
  3098. }
  3099. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  3100. fun onMessageEvent(webSocketCommunicationEvent: WebSocketCommunicationEvent) {
  3101. /*
  3102. switch (webSocketCommunicationEvent.getType()) {
  3103. case "refreshChat":
  3104. if (
  3105. webSocketCommunicationEvent
  3106. .getHashMap().get(BundleKeys.KEY_INTERNAL_USER_ID)
  3107. .equals(Long.toString(conversationUser.getId()))
  3108. ) {
  3109. if (roomToken.equals(webSocketCommunicationEvent.getHashMap().get(BundleKeys.KEY_ROOM_TOKEN))) {
  3110. pullChatMessages(2);
  3111. }
  3112. }
  3113. break;
  3114. default:
  3115. }*/
  3116. }
  3117. @Subscribe(threadMode = ThreadMode.BACKGROUND)
  3118. fun onMessageEvent(userMentionClickEvent: UserMentionClickEvent) {
  3119. if (currentConversation?.type != ConversationEnums.ConversationType.ROOM_TYPE_ONE_TO_ONE_CALL ||
  3120. currentConversation?.name != userMentionClickEvent.userId
  3121. ) {
  3122. var apiVersion = 1
  3123. // FIXME Fix API checking with guests?
  3124. if (conversationUser != null) {
  3125. apiVersion = ApiUtils.getConversationApiVersion(conversationUser!!, intArrayOf(ApiUtils.API_V4, 1))
  3126. }
  3127. val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom(
  3128. apiVersion,
  3129. conversationUser?.baseUrl!!,
  3130. "1",
  3131. null,
  3132. userMentionClickEvent.userId,
  3133. null
  3134. )
  3135. chatViewModel.createRoom(
  3136. credentials!!,
  3137. retrofitBucket.url!!,
  3138. retrofitBucket.queryMap!!
  3139. )
  3140. }
  3141. }
  3142. fun sendPictureFromCamIntent() {
  3143. if (!permissionUtil.isCameraPermissionGranted()) {
  3144. requestCameraPermissions()
  3145. } else {
  3146. startPickCameraIntentForResult.launch(TakePhotoActivity.createIntent(context))
  3147. }
  3148. }
  3149. fun sendVideoFromCamIntent() {
  3150. if (!permissionUtil.isCameraPermissionGranted()) {
  3151. requestCameraPermissions()
  3152. } else {
  3153. Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
  3154. takeVideoIntent.resolveActivity(packageManager)?.also {
  3155. val videoFile: File? = try {
  3156. val outputDir = context.cacheDir
  3157. val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT)
  3158. val date = dateFormat.format(Date())
  3159. val videoName = String.format(
  3160. context.resources.getString(R.string.nc_video_filename),
  3161. date
  3162. )
  3163. File("$outputDir/$videoName$VIDEO_SUFFIX")
  3164. } catch (e: IOException) {
  3165. Snackbar.make(binding.root, R.string.nc_common_error_sorry, Snackbar.LENGTH_LONG).show()
  3166. Log.e(TAG, "error while creating video file", e)
  3167. null
  3168. }
  3169. videoFile?.also {
  3170. videoURI = FileProvider.getUriForFile(context, context.packageName, it)
  3171. takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI)
  3172. startPickCameraIntentForResult.launch(takeVideoIntent)
  3173. }
  3174. }
  3175. }
  3176. }
  3177. }
  3178. fun createPoll() {
  3179. val pollVoteDialog = PollCreateDialogFragment.newInstance(
  3180. roomToken
  3181. )
  3182. pollVoteDialog.show(supportFragmentManager, TAG)
  3183. }
  3184. fun jumpToQuotedMessage(parentMessage: ChatMessage) {
  3185. var foundMessage = false
  3186. for (position in 0 until (adapter!!.items.size)) {
  3187. val currentItem = adapter?.items?.get(position)?.item
  3188. if (currentItem is ChatMessage && currentItem.id == parentMessage.id) {
  3189. layoutManager!!.scrollToPosition(position)
  3190. foundMessage = true
  3191. break
  3192. }
  3193. }
  3194. if (!foundMessage) {
  3195. Log.d(TAG, "quoted message with id " + parentMessage.id + " was not found in adapter")
  3196. // TODO: show better info
  3197. // TODO: improve handling how this can be avoided. E.g. loading chat until message is reached...
  3198. Snackbar.make(binding.root, "Message was not found", Snackbar.LENGTH_LONG).show()
  3199. }
  3200. }
  3201. override fun joinAudioCall() {
  3202. startACall(true, false)
  3203. }
  3204. override fun joinVideoCall() {
  3205. startACall(false, false)
  3206. }
  3207. override fun editTemporaryMessage(id: Int, newMessage: String) {
  3208. messageInputViewModel.editQueuedMessage(currentConversation!!.internalId, id, newMessage)
  3209. adapter?.notifyDataSetChanged() // TODO optimize this
  3210. }
  3211. override fun deleteTemporaryMessage(id: Int) {
  3212. messageInputViewModel.removeFromQueue(currentConversation!!.internalId, id)
  3213. var i = 0
  3214. val max = messageInputViewModel.messageQueueSizeFlow.value?.plus(1)
  3215. for (item in adapter?.items!!) {
  3216. if (i > max!! && max < 1) break
  3217. if (item.item is ChatMessage &&
  3218. (item.item as ChatMessage).isTempMessage &&
  3219. (item.item as ChatMessage).tempMessageId == id
  3220. ) {
  3221. val index = adapter?.items!!.indexOf(item)
  3222. adapter?.items!!.removeAt(index)
  3223. adapter?.notifyItemRemoved(index)
  3224. break
  3225. }
  3226. i++
  3227. }
  3228. }
  3229. private fun logConversationInfos(methodName: String) {
  3230. Log.d(TAG, " |-----------------------------------------------")
  3231. Log.d(TAG, " | method: $methodName")
  3232. Log.d(TAG, " | ChatActivity: " + System.identityHashCode(this).toString())
  3233. Log.d(TAG, " | roomToken: $roomToken")
  3234. Log.d(TAG, " | currentConversation?.displayName: ${currentConversation?.displayName}")
  3235. Log.d(TAG, " | sessionIdAfterRoomJoined: $sessionIdAfterRoomJoined")
  3236. Log.d(TAG, " |-----------------------------------------------")
  3237. }
  3238. fun shareMessageText(message: String) {
  3239. val sendIntent: Intent = Intent().apply {
  3240. action = Intent.ACTION_SEND
  3241. putExtra(Intent.EXTRA_TEXT, message)
  3242. type = "text/plain"
  3243. }
  3244. val shareIntent = Intent.createChooser(sendIntent, getString(R.string.share))
  3245. startActivity(shareIntent)
  3246. }
  3247. companion object {
  3248. val TAG = ChatActivity::class.simpleName
  3249. private const val CONTENT_TYPE_CALL_STARTED: Byte = 1
  3250. private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 2
  3251. private const val CONTENT_TYPE_UNREAD_NOTICE_MESSAGE: Byte = 3
  3252. private const val CONTENT_TYPE_LOCATION: Byte = 4
  3253. private const val CONTENT_TYPE_VOICE_MESSAGE: Byte = 5
  3254. private const val CONTENT_TYPE_POLL: Byte = 6
  3255. private const val CONTENT_TYPE_LINK_PREVIEW: Byte = 7
  3256. private const val CONTENT_TYPE_DECK_CARD: Byte = 8
  3257. private const val CONTENT_TYPE_TEMP: Byte = 9
  3258. private const val UNREAD_MESSAGES_MARKER_ID = -1
  3259. private const val CALL_STARTED_ID = -2
  3260. private const val GET_ROOM_INFO_DELAY_NORMAL: Long = 30000
  3261. private const val GET_ROOM_INFO_DELAY_LOBBY: Long = 5000
  3262. private const val AGE_THRESHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
  3263. private const val REQUEST_SHARE_FILE_PERMISSION: Int = 221
  3264. private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
  3265. private const val REQUEST_READ_CONTACT_PERMISSION = 234
  3266. private const val REQUEST_CAMERA_PERMISSION = 223
  3267. private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
  3268. private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
  3269. private const val VIDEO_SUFFIX = ".mp4"
  3270. private const val FULLY_OPAQUE_INT: Int = 255
  3271. private const val SEMI_TRANSPARENT_INT: Int = 99
  3272. private const val VOICE_MESSAGE_SEEKBAR_BASE = 1000
  3273. private const val NO_PREVIOUS_MESSAGE_ID: Int = -1
  3274. private const val TOOLBAR_AVATAR_RATIO = 1.5
  3275. private const val STATUS_SIZE_IN_DP = 9f
  3276. private const val HTTP_BAD_REQUEST = 400
  3277. private const val HTTP_FORBIDDEN = 403
  3278. private const val HTTP_NOT_FOUND = 404
  3279. private const val MESSAGE_PULL_LIMIT = 100
  3280. private const val INVITE_LENGTH = 6
  3281. private const val ACTOR_LENGTH = 6
  3282. private const val CHUNK_SIZE: Int = 10
  3283. private const val ONE_SECOND_IN_MILLIS = 1000
  3284. private const val WHITESPACE = " "
  3285. private const val COMMA = ", "
  3286. private const val TYPING_INDICATOR_ANIMATION_DURATION = 200L
  3287. private const val TYPING_INDICATOR_MAX_NAME_LENGTH = 14
  3288. private const val TYPING_INDICATOR_POSITION_VISIBLE = -18f
  3289. private const val TYPING_INDICATOR_POSITION_HIDDEN = -1f
  3290. private const val MILISEC_15: Long = 15
  3291. private const val CURRENT_AUDIO_MESSAGE_KEY = "CURRENT_AUDIO_MESSAGE"
  3292. private const val CURRENT_AUDIO_POSITION_KEY = "CURRENT_AUDIO_POSITION"
  3293. private const val CURRENT_AUDIO_WAS_PLAYING_KEY = "CURRENT_AUDIO_PLAYING"
  3294. private const val RESUME_AUDIO_TAG = "RESUME_AUDIO_TAG"
  3295. private const val DELAY_TO_SHOW_PROGRESS_BAR = 1000L
  3296. private const val FIVE_MINUTES_IN_SECONDS: Long = 300
  3297. }
  3298. }