ChatController.kt 113 KB

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