ChatController.kt 138 KB

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