ChatController.kt 132 KB

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