ChatController.kt 132 KB

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