ChatController.kt 120 KB

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