ChatController.kt 128 KB

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