ChatController.kt 132 KB

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