ChatController.kt 138 KB

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