ChatController.kt 132 KB

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