ChatController.kt 130 KB

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