ChatController.kt 132 KB

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