ComposeMessageFragment.java 151 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2013-2022 Threema GmbH
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License, version 3,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU Affero General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU Affero General Public License
  19. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. */
  21. package ch.threema.app.fragments;
  22. import android.Manifest;
  23. import android.annotation.SuppressLint;
  24. import android.app.Activity;
  25. import android.content.ClipData;
  26. import android.content.ClipboardManager;
  27. import android.content.Context;
  28. import android.content.Intent;
  29. import android.content.pm.PackageManager;
  30. import android.content.res.AssetFileDescriptor;
  31. import android.content.res.Configuration;
  32. import android.graphics.Bitmap;
  33. import android.graphics.Paint;
  34. import android.media.AudioAttributes;
  35. import android.media.AudioManager;
  36. import android.media.MediaPlayer;
  37. import android.net.Uri;
  38. import android.os.AsyncTask;
  39. import android.os.Build;
  40. import android.os.Bundle;
  41. import android.os.Handler;
  42. import android.os.SystemClock;
  43. import android.os.Vibrator;
  44. import android.text.Editable;
  45. import android.text.TextUtils;
  46. import android.text.TextWatcher;
  47. import android.text.format.DateUtils;
  48. import android.util.Pair;
  49. import android.view.Gravity;
  50. import android.view.HapticFeedbackConstants;
  51. import android.view.KeyEvent;
  52. import android.view.LayoutInflater;
  53. import android.view.Menu;
  54. import android.view.MenuInflater;
  55. import android.view.MenuItem;
  56. import android.view.MotionEvent;
  57. import android.view.View;
  58. import android.view.ViewGroup;
  59. import android.view.ViewStub;
  60. import android.view.WindowManager;
  61. import android.view.animation.LinearInterpolator;
  62. import android.view.inputmethod.EditorInfo;
  63. import android.widget.AbsListView;
  64. import android.widget.Filter;
  65. import android.widget.FrameLayout;
  66. import android.widget.ImageButton;
  67. import android.widget.ImageView;
  68. import android.widget.LinearLayout;
  69. import android.widget.ProgressBar;
  70. import android.widget.RelativeLayout;
  71. import android.widget.ScrollView;
  72. import android.widget.TextView;
  73. import android.widget.Toast;
  74. import com.google.android.material.snackbar.Snackbar;
  75. import org.slf4j.Logger;
  76. import org.slf4j.LoggerFactory;
  77. import java.io.File;
  78. import java.sql.SQLException;
  79. import java.text.SimpleDateFormat;
  80. import java.util.ArrayList;
  81. import java.util.Arrays;
  82. import java.util.Collections;
  83. import java.util.Comparator;
  84. import java.util.Date;
  85. import java.util.HashMap;
  86. import java.util.List;
  87. import java.util.Map;
  88. import java.util.concurrent.CopyOnWriteArrayList;
  89. import java.util.regex.Pattern;
  90. import androidx.annotation.AnyThread;
  91. import androidx.annotation.ColorInt;
  92. import androidx.annotation.NonNull;
  93. import androidx.annotation.Nullable;
  94. import androidx.annotation.UiThread;
  95. import androidx.annotation.WorkerThread;
  96. import androidx.appcompat.app.ActionBar;
  97. import androidx.appcompat.app.AppCompatActivity;
  98. import androidx.appcompat.view.ActionMode;
  99. import androidx.appcompat.widget.SearchView;
  100. import androidx.coordinatorlayout.widget.CoordinatorLayout;
  101. import androidx.core.content.ContextCompat;
  102. import androidx.core.content.pm.ShortcutManagerCompat;
  103. import androidx.core.graphics.ColorUtils;
  104. import androidx.core.view.MenuItemCompat;
  105. import androidx.core.view.OnApplyWindowInsetsListener;
  106. import androidx.core.view.ViewCompat;
  107. import androidx.core.view.WindowInsetsCompat;
  108. import androidx.fragment.app.Fragment;
  109. import androidx.fragment.app.FragmentActivity;
  110. import androidx.lifecycle.DefaultLifecycleObserver;
  111. import androidx.lifecycle.LifecycleOwner;
  112. import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  113. import androidx.transition.Slide;
  114. import androidx.transition.Transition;
  115. import androidx.transition.TransitionManager;
  116. import ch.threema.app.R;
  117. import ch.threema.app.ThreemaApplication;
  118. import ch.threema.app.actions.SendAction;
  119. import ch.threema.app.actions.TextMessageSendAction;
  120. import ch.threema.app.activities.ComposeMessageActivity;
  121. import ch.threema.app.activities.ContactDetailActivity;
  122. import ch.threema.app.activities.ContactNotificationsActivity;
  123. import ch.threema.app.activities.DistributionListAddActivity;
  124. import ch.threema.app.activities.GroupNotificationsActivity;
  125. import ch.threema.app.activities.HomeActivity;
  126. import ch.threema.app.activities.MediaGalleryActivity;
  127. import ch.threema.app.activities.RecipientListBaseActivity;
  128. import ch.threema.app.activities.SendMediaActivity;
  129. import ch.threema.app.activities.TextChatBubbleActivity;
  130. import ch.threema.app.activities.ThreemaActivity;
  131. import ch.threema.app.activities.ThreemaToolbarActivity;
  132. import ch.threema.app.activities.WorkExplainActivity;
  133. import ch.threema.app.activities.ballot.BallotOverviewActivity;
  134. import ch.threema.app.adapters.ComposeMessageAdapter;
  135. import ch.threema.app.adapters.decorators.ChatAdapterDecorator;
  136. import ch.threema.app.asynctasks.EmptyChatAsyncTask;
  137. import ch.threema.app.cache.ThumbnailCache;
  138. import ch.threema.app.dialogs.ExpandableTextEntryDialog;
  139. import ch.threema.app.dialogs.GenericAlertDialog;
  140. import ch.threema.app.dialogs.GenericProgressDialog;
  141. import ch.threema.app.dialogs.MessageDetailDialog;
  142. import ch.threema.app.dialogs.SelectorDialog;
  143. import ch.threema.app.emojis.EmojiButton;
  144. import ch.threema.app.emojis.EmojiMarkupUtil;
  145. import ch.threema.app.emojis.EmojiPicker;
  146. import ch.threema.app.emojis.EmojiTextView;
  147. import ch.threema.app.grouplinks.IncomingGroupRequestActivity;
  148. import ch.threema.app.grouplinks.OpenGroupRequestNoticeView;
  149. import ch.threema.app.listeners.BallotListener;
  150. import ch.threema.app.listeners.ContactListener;
  151. import ch.threema.app.listeners.ContactTypingListener;
  152. import ch.threema.app.listeners.ConversationListener;
  153. import ch.threema.app.listeners.GroupListener;
  154. import ch.threema.app.listeners.MessageListener;
  155. import ch.threema.app.listeners.MessagePlayerListener;
  156. import ch.threema.app.listeners.QRCodeScanListener;
  157. import ch.threema.app.managers.ListenerManager;
  158. import ch.threema.app.managers.ServiceManager;
  159. import ch.threema.app.mediaattacher.MediaAttachActivity;
  160. import ch.threema.app.mediaattacher.MediaFilterQuery;
  161. import ch.threema.app.messagereceiver.ContactMessageReceiver;
  162. import ch.threema.app.messagereceiver.MessageReceiver;
  163. import ch.threema.app.routines.ReadMessagesRoutine;
  164. import ch.threema.app.services.ContactService;
  165. import ch.threema.app.services.ConversationService;
  166. import ch.threema.app.services.DeadlineListService;
  167. import ch.threema.app.services.DeviceService;
  168. import ch.threema.app.services.DistributionListService;
  169. import ch.threema.app.services.DownloadService;
  170. import ch.threema.app.services.FileService;
  171. import ch.threema.app.services.GroupService;
  172. import ch.threema.app.services.IdListService;
  173. import ch.threema.app.services.MessageService;
  174. import ch.threema.app.services.NotificationService;
  175. import ch.threema.app.services.PreferenceService;
  176. import ch.threema.app.services.RingtoneService;
  177. import ch.threema.app.services.UserService;
  178. import ch.threema.app.services.WallpaperService;
  179. import ch.threema.app.services.ballot.BallotService;
  180. import ch.threema.app.services.license.LicenseService;
  181. import ch.threema.app.services.messageplayer.MessagePlayerService;
  182. import ch.threema.app.ui.AvatarView;
  183. import ch.threema.app.ui.ContentCommitComposeEditText;
  184. import ch.threema.app.ui.ConversationListView;
  185. import ch.threema.app.ui.DebouncedOnClickListener;
  186. import ch.threema.app.ui.ListViewSwipeListener;
  187. import ch.threema.app.ui.MentionSelectorPopup;
  188. import ch.threema.app.ui.OpenBallotNoticeView;
  189. import ch.threema.app.ui.QRCodePopup;
  190. import ch.threema.app.ui.SelectorDialogItem;
  191. import ch.threema.app.ui.SendButton;
  192. import ch.threema.app.ui.SingleToast;
  193. import ch.threema.app.ui.TooltipPopup;
  194. import ch.threema.app.ui.TypingIndicatorTextWatcher;
  195. import ch.threema.app.ui.VerificationLevelImageView;
  196. import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
  197. import ch.threema.app.utils.AnimationUtil;
  198. import ch.threema.app.utils.AppRestrictionUtil;
  199. import ch.threema.app.utils.BallotUtil;
  200. import ch.threema.app.utils.ConfigUtils;
  201. import ch.threema.app.utils.ContactUtil;
  202. import ch.threema.app.utils.ConversationUtil;
  203. import ch.threema.app.utils.DialogUtil;
  204. import ch.threema.app.utils.EditTextUtil;
  205. import ch.threema.app.utils.FileUtil;
  206. import ch.threema.app.utils.IntentDataUtil;
  207. import ch.threema.app.utils.LinkifyUtil;
  208. import ch.threema.app.utils.LocaleUtil;
  209. import ch.threema.app.utils.LogUtil;
  210. import ch.threema.app.utils.MediaPlayerStateWrapper;
  211. import ch.threema.app.utils.MessageUtil;
  212. import ch.threema.app.utils.NameUtil;
  213. import ch.threema.app.utils.NavigationUtil;
  214. import ch.threema.app.utils.QuoteUtil;
  215. import ch.threema.app.utils.RuntimeUtil;
  216. import ch.threema.app.utils.ShortcutUtil;
  217. import ch.threema.app.utils.TestUtil;
  218. import ch.threema.app.utils.ToolbarUtil;
  219. import ch.threema.app.voicemessage.VoiceRecorderActivity;
  220. import ch.threema.app.voip.listeners.VoipCallEventListener;
  221. import ch.threema.app.voip.managers.VoipListenerManager;
  222. import ch.threema.app.voip.services.VoipStateService;
  223. import ch.threema.app.voip.util.VoipUtil;
  224. import ch.threema.domain.models.IdentityType;
  225. import ch.threema.domain.protocol.csp.messages.file.FileData;
  226. import ch.threema.storage.DatabaseServiceNew;
  227. import ch.threema.storage.models.AbstractMessageModel;
  228. import ch.threema.storage.models.ContactModel;
  229. import ch.threema.storage.models.ConversationModel;
  230. import ch.threema.storage.models.DateSeparatorMessageModel;
  231. import ch.threema.storage.models.DistributionListMessageModel;
  232. import ch.threema.storage.models.DistributionListModel;
  233. import ch.threema.storage.models.FirstUnreadMessageModel;
  234. import ch.threema.storage.models.GroupMessageModel;
  235. import ch.threema.storage.models.GroupModel;
  236. import ch.threema.storage.models.MessageState;
  237. import ch.threema.storage.models.MessageType;
  238. import ch.threema.storage.models.ballot.BallotModel;
  239. import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
  240. import static ch.threema.app.ThreemaApplication.getAppContext;
  241. import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_AUDIORECORDER;
  242. import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_LIFECYCLE;
  243. import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_VOIP;
  244. import static ch.threema.app.utils.LinkifyUtil.DIALOG_TAG_CONFIRM_LINK;
  245. import static ch.threema.app.utils.ShortcutUtil.TYPE_CHAT;
  246. public class ComposeMessageFragment extends Fragment implements
  247. LifecycleOwner,
  248. DefaultLifecycleObserver,
  249. SwipeRefreshLayout.OnRefreshListener,
  250. GenericAlertDialog.DialogClickListener,
  251. ChatAdapterDecorator.ActionModeStatus,
  252. SelectorDialog.SelectorDialogClickListener,
  253. EmojiPicker.EmojiPickerListener,
  254. MentionSelectorPopup.MentionSelectorListener,
  255. OpenBallotNoticeView.VisibilityListener,
  256. ThreemaToolbarActivity.OnSoftKeyboardChangedListener,
  257. ExpandableTextEntryDialog.ExpandableTextEntryDialogClickListener {
  258. private static final Logger logger = LoggerFactory.getLogger(ComposeMessageFragment.class);
  259. private static final String CONFIRM_TAG_DELETE_DISTRIBUTION_LIST = "deleteDistributionList";
  260. public static final String DIALOG_TAG_CONFIRM_CALL = "dtcc";
  261. private static final String DIALOG_TAG_CHOOSE_SHORTCUT_TYPE = "st";
  262. private static final String DIALOG_TAG_EMPTY_CHAT = "ccc";
  263. private static final String DIALOG_TAG_CONFIRM_BLOCK = "block";
  264. private static final String DIALOG_TAG_DECRYPTING_MESSAGES = "dcr";
  265. private static final String DIALOG_TAG_SEARCHING = "src";
  266. private static final String DIALOG_TAG_LOADING_MESSAGES = "loadm";
  267. public static final String EXTRA_API_MESSAGE_ID = "apimsgid";
  268. public static final String EXTRA_SEARCH_QUERY = "searchQuery";
  269. public static final String EXTRA_LAST_MEDIA_SEARCH_QUERY = "searchMediaQuery";
  270. public static final String EXTRA_LAST_MEDIA_TYPE_QUERY = "searchMediaType";
  271. private static final int PERMISSION_REQUEST_SAVE_MESSAGE = 2;
  272. private static final int PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE = 7;
  273. private static final int PERMISSION_REQUEST_ATTACH_CAMERA = 8;
  274. private static final int PERMISSION_REQUEST_ATTACH_CAMERA_VIDEO = 11;
  275. private static final int ACTIVITY_ID_VOICE_RECORDER = 9731;
  276. public static final long VIBRATION_MSEC = 300;
  277. private static final long MESSAGE_PAGE_SIZE = 100;
  278. public static final int SCROLLBUTTON_VIEW_TIMEOUT = 3000;
  279. private static final int SMOOTHSCROLL_THRESHOLD = 10;
  280. private static final int MAX_SELECTED_ITEMS = 100; // may not be larger than MESSAGE_PAGE_SIZE
  281. private static final int MAX_FORWARDABLE_ITEMS = 50;
  282. private static final int CONTEXT_MENU_BOLD = 700;
  283. private static final int CONTEXT_MENU_ITALIC = 701;
  284. private static final int CONTEXT_MENU_STRIKETHRU = 702;
  285. private static final int CONTEXT_MENU_GROUP = 22100;
  286. private static final String CAMERA_URI = "camera_uri";
  287. private ContentCommitComposeEditText messageText;
  288. private SendButton sendButton;
  289. private ImageButton attachButton, cameraButton;
  290. private ContactModel contactModel;
  291. private MessageReceiver messageReceiver;
  292. private AudioManager audioManager;
  293. private ConversationListView convListView;
  294. private ComposeMessageAdapter composeMessageAdapter;
  295. private View isTypingView;
  296. private MenuItem mutedMenuItem = null, blockMenuItem = null, deleteDistributionListItem = null,
  297. callItem = null, shortCutItem = null, showOpenBallotWindowMenuItem = null,
  298. showBallotsMenuItem = null, showAllGroupRequestsMenuItem = null,
  299. showOpenGroupRequestsMenuItem = null;
  300. private TextView dateTextView;
  301. private ActionMode actionMode = null;
  302. private ActionMode searchActionMode = null;
  303. private ImageView quickscrollDownView = null, quickscrollUpView = null;
  304. private FrameLayout dateView = null;
  305. private FrameLayout bottomPanel = null;
  306. private String identity;
  307. private Integer groupId = 0, distributionListId = 0;
  308. private Uri cameraUri;
  309. private long intentTimestamp = 0L;
  310. private int longClickItem = AbsListView.INVALID_POSITION;
  311. private int listViewTop = 0, lastFirstVisibleItem = -1;
  312. private Snackbar deleteSnackbar;
  313. private TypingIndicatorTextWatcher typingIndicatorTextWatcher;
  314. private Map<String, Integer> identityColors;
  315. private MediaFilterQuery lastMediaFilter;
  316. private PreferenceService preferenceService;
  317. private ContactService contactService;
  318. private MessageService messageService;
  319. private NotificationService notificationService;
  320. private IdListService blackListIdentityService;
  321. private ConversationService conversationService;
  322. private DeviceService deviceService;
  323. private WallpaperService wallpaperService;
  324. private DeadlineListService mutedChatsListService, mentionOnlyChatsListService, hiddenChatsListService;
  325. private RingtoneService ringtoneService;
  326. private UserService userService;
  327. private FileService fileService;
  328. private VoipStateService voipStateService;
  329. private DownloadService downloadService;
  330. private LicenseService licenseService;
  331. private boolean listUpdateInProgress = false, isPaused = false;
  332. private final List<AbstractMessageModel> unreadMessages = new ArrayList<>();
  333. private final List<AbstractMessageModel> messageValues = new ArrayList<>();
  334. private final List<AbstractMessageModel> selectedMessages = new ArrayList<>(1);
  335. private final List<Pair<AbstractMessageModel, Integer>> deleteableMessages = new ArrayList<>(1);
  336. private EmojiMarkupUtil emojiMarkupUtil;
  337. private EmojiPicker emojiPicker;
  338. private EmojiButton emojiButton;
  339. private SwipeRefreshLayout swipeRefreshLayout;
  340. private Integer currentPageReferenceId = null;
  341. private EmojiTextView actionBarTitleTextView;
  342. private TextView actionBarSubtitleTextView;
  343. private VerificationLevelImageView actionBarSubtitleImageView;
  344. private AvatarView actionBarAvatarView;
  345. private ImageView wallpaperView;
  346. private ActionBar actionBar;
  347. private MentionSelectorPopup mentionPopup;
  348. private TooltipPopup workTooltipPopup;
  349. private OpenBallotNoticeView openBallotNoticeView;
  350. private OpenGroupRequestNoticeView openGroupRequestNoticeView;
  351. private ComposeMessageActivity activity;
  352. private View fragmentView;
  353. private CoordinatorLayout coordinatorLayout;
  354. private BallotService ballotService;
  355. private DatabaseServiceNew databaseServiceNew;
  356. private LayoutInflater layoutInflater;
  357. private ListViewSwipeListener listViewSwipeListener;
  358. private GroupService groupService;
  359. private boolean isGroupChat = false;
  360. private GroupModel groupModel;
  361. private Date listInitializedAt;
  362. private boolean isDistributionListChat = false;
  363. private DistributionListService distributionListService;
  364. private DistributionListModel distributionListModel;
  365. private MessagePlayerService messagePlayerService;
  366. private int listInstancePosition = AbsListView.INVALID_POSITION;
  367. private int listInstanceTop = 0;
  368. private String listInstanceReceiverId = null;
  369. private String conversationUid = null;
  370. private int unreadCount = 0;
  371. private final QuoteInfo quoteInfo = new QuoteInfo();
  372. private TextView searchCounter;
  373. private ProgressBar searchProgress;
  374. private ImageView searchNextButton, searchPreviousButton;
  375. @SuppressLint("SimpleDateFormat")
  376. private final SimpleDateFormat dayFormatter = new SimpleDateFormat("yyyyMMdd");
  377. private ThumbnailCache<?> thumbnailCache = null;
  378. private static class QuoteInfo {
  379. private FrameLayout quotePanel;
  380. private TextView quoteTextView, quoteIdentityView;
  381. private String quoteText, quoteIdentity;
  382. private View quoteBar;
  383. private ImageView quoteThumbnail;
  384. private ImageView quoteTypeImage;
  385. private AbstractMessageModel messageModel;
  386. }
  387. @Override
  388. public boolean getActionModeEnabled() {
  389. return actionMode != null;
  390. }
  391. private final MessageService.MessageFilter nextMessageFilter = new MessageService.MessageFilter() {
  392. @Override
  393. public long getPageSize() {
  394. return MESSAGE_PAGE_SIZE;
  395. }
  396. @Override
  397. public Integer getPageReferenceId() {
  398. return getCurrentPageReferenceId();
  399. }
  400. @Override
  401. public boolean withStatusMessages() {
  402. return true;
  403. }
  404. @Override
  405. public boolean withUnsaved() {
  406. return false;
  407. }
  408. @Override
  409. public boolean onlyUnread() {
  410. return false;
  411. }
  412. @Override
  413. public boolean onlyDownloaded() {
  414. return false;
  415. }
  416. @Override
  417. public MessageType[] types() {
  418. return null;
  419. }
  420. @Override
  421. public int[] contentTypes() {
  422. return null;
  423. }
  424. };
  425. // handler to remove dateview button after a certain time
  426. private final Handler dateViewHandler = new Handler();
  427. private final Runnable dateViewTask = () -> RuntimeUtil.runOnUiThread(() -> {
  428. if (dateView != null && dateView.getVisibility() == View.VISIBLE) {
  429. AnimationUtil.slideOutAnimation(dateView, false, 1f, null);
  430. AnimationUtil.setFadingVisibility(quickscrollUpView, View.GONE);
  431. AnimationUtil.setFadingVisibility(quickscrollDownView, View.GONE);
  432. }
  433. });
  434. // Listeners
  435. private final VoipCallEventListener voipCallEventListener = new VoipCallEventListener() {
  436. @Override
  437. public void onRinging(String peerIdentity) {
  438. }
  439. @Override
  440. public void onStarted(String peerIdentity, boolean outgoing) {
  441. logger.debug("VoipCallEventListener onStarted");
  442. updateVoipCallMenuItem(false);
  443. if (messagePlayerService != null) {
  444. messagePlayerService.pauseAll(SOURCE_VOIP);
  445. }
  446. }
  447. @Override
  448. public void onFinished(@NonNull String peerIdentity, boolean outgoing, int duration) {
  449. logger.debug("VoipCallEventListener onFinished");
  450. updateVoipCallMenuItem(true);
  451. }
  452. @Override
  453. public void onRejected(String peerIdentity, boolean outgoing, byte reason) {
  454. logger.debug("VoipCallEventListener onRejected");
  455. updateVoipCallMenuItem(true);
  456. }
  457. @Override
  458. public void onMissed(String peerIdentity, boolean accepted) {
  459. logger.debug("VoipCallEventListener onMissed");
  460. updateVoipCallMenuItem(true);
  461. }
  462. @Override
  463. public void onAborted(String peerIdentity) {
  464. logger.debug("VoipCallEventListener onAborted");
  465. updateVoipCallMenuItem(true); }
  466. };
  467. private final MessageListener messageListener = new MessageListener() {
  468. @Override
  469. public void onNew(final AbstractMessageModel newMessage) {
  470. if (newMessage != null) {
  471. RuntimeUtil.runOnUiThread(() -> {
  472. if (isAdded() && !isDetached() && !isRemoving()) {
  473. if (newMessage.isOutbox()) {
  474. if (addMessageToList(newMessage)) {
  475. if (!newMessage.isStatusMessage() && (newMessage.getType() != MessageType.VOIP_STATUS)) {
  476. playSentSound();
  477. }
  478. }
  479. } else {
  480. if (addMessageToList(newMessage) && !isPaused) {
  481. if (!newMessage.isStatusMessage() && (newMessage.getType() != MessageType.VOIP_STATUS)) {
  482. playReceivedSound();
  483. }
  484. }
  485. }
  486. }
  487. });
  488. }
  489. }
  490. @Override
  491. public void onModified(final List<AbstractMessageModel> modifiedMessageModels) {
  492. //replace model
  493. synchronized (messageValues) {
  494. for (final AbstractMessageModel modifiedMessageModel : modifiedMessageModels) {
  495. if (modifiedMessageModel.getId() != 0) {
  496. for (int n = 0; n < messageValues.size(); n++) {
  497. AbstractMessageModel listModel = messageValues.get(n);
  498. if (listModel != null && listModel.getId() == modifiedMessageModel.getId()) {
  499. //if the changed message is different to the created
  500. if (modifiedMessageModel != listModel) {
  501. //replace item
  502. messageValues.set(n, modifiedMessageModel);
  503. }
  504. break;
  505. }
  506. }
  507. }
  508. }
  509. }
  510. RuntimeUtil.runOnUiThread(() -> {
  511. if (composeMessageAdapter != null) {
  512. composeMessageAdapter.notifyDataSetChanged();
  513. }
  514. });
  515. }
  516. @Override
  517. public void onRemoved(final AbstractMessageModel removedMessageModel) {
  518. RuntimeUtil.runOnUiThread(() -> {
  519. if (TestUtil.required(composeMessageAdapter, removedMessageModel)) {
  520. composeMessageAdapter.remove(removedMessageModel);
  521. composeMessageAdapter.notifyDataSetChanged();
  522. }
  523. });
  524. }
  525. @Override
  526. public void onRemoved(List<AbstractMessageModel> removedMessageModels) {
  527. RuntimeUtil.runOnUiThread(() -> {
  528. if (TestUtil.required(composeMessageAdapter, removedMessageModels)) {
  529. for (AbstractMessageModel removedMessageModel : removedMessageModels) {
  530. composeMessageAdapter.remove(removedMessageModel);
  531. }
  532. RuntimeUtil.runOnUiThread(() -> {
  533. composeMessageAdapter.notifyDataSetChanged();
  534. });
  535. }
  536. });
  537. }
  538. @Override
  539. public void onProgressChanged(AbstractMessageModel messageModel, int newProgress) {
  540. //ignore
  541. RuntimeUtil.runOnUiThread(() -> {
  542. if (composeMessageAdapter != null) {
  543. composeMessageAdapter.notifyDataSetChanged();
  544. }
  545. });
  546. }
  547. };
  548. private final GroupListener groupListener = new GroupListener() {
  549. @Override
  550. public void onCreate(GroupModel newGroupModel) {
  551. //do nothing
  552. }
  553. @Override
  554. public void onRename(GroupModel groupModel) {
  555. updateToolBarTitleInUIThread();
  556. }
  557. @Override
  558. public void onUpdatePhoto(GroupModel groupModel) {
  559. updateToolBarTitleInUIThread();
  560. }
  561. @Override
  562. public void onRemove(GroupModel groupModel) {
  563. if (isGroupChat && groupId != null && groupId == groupModel.getId()) {
  564. RuntimeUtil.runOnUiThread(new Runnable() {
  565. @Override
  566. public void run() {
  567. if (activity != null) {
  568. activity.finish();
  569. }
  570. }
  571. });
  572. }
  573. }
  574. @Override
  575. public void onNewMember(GroupModel group, String newIdentity, int previousMemberCount) {
  576. updateToolBarTitleInUIThread();
  577. }
  578. @Override
  579. public void onMemberLeave(GroupModel group, String identity, int previousMemberCount) {
  580. updateToolBarTitleInUIThread();
  581. }
  582. @Override
  583. public void onMemberKicked(GroupModel group, String identity, int previousMemberCount) {
  584. updateToolBarTitleInUIThread();
  585. }
  586. @Override
  587. public void onUpdate(GroupModel groupModel) {
  588. updateToolBarTitleInUIThread();
  589. }
  590. @Override
  591. public void onLeave(GroupModel groupModel) {
  592. if (isGroupChat && groupId != null && groupId == groupModel.getId()) {
  593. RuntimeUtil.runOnUiThread(new Runnable() {
  594. @Override
  595. public void run() {
  596. if (activity != null) {
  597. activity.finish();
  598. }
  599. }
  600. });
  601. }
  602. }
  603. };
  604. private final ContactListener contactListener = new ContactListener() {
  605. @Override
  606. public void onModified(final ContactModel modifiedContactModel) {
  607. RuntimeUtil.runOnUiThread(() -> updateContactModelData(modifiedContactModel));
  608. }
  609. @Override
  610. public void onAvatarChanged(ContactModel contactModel) {
  611. updateToolBarTitleInUIThread();
  612. }
  613. @Override
  614. public void onRemoved(ContactModel removedContactModel) {
  615. if (contactModel != null && contactModel.equals(removedContactModel)) {
  616. // our contact has been removed. finish activity.
  617. RuntimeUtil.runOnUiThread(() -> {
  618. if (activity != null) {
  619. activity.finish();
  620. }
  621. });
  622. }
  623. }
  624. @Override
  625. public boolean handle(String handleIdentity) {
  626. //handle every contact change, affected contact models:
  627. //1. the contact of the chat
  628. //2. a contact as group member
  629. return true;
  630. }
  631. };
  632. private final ContactTypingListener contactTypingListener = new ContactTypingListener() {
  633. @Override
  634. public void onContactIsTyping(final ContactModel fromContact, final boolean isTyping) {
  635. RuntimeUtil.runOnUiThread(() -> {
  636. if (contactModel != null && fromContact.getIdentity().equals(contactModel.getIdentity())) {
  637. contactTypingStateChanged(isTyping);
  638. }
  639. });
  640. }
  641. };
  642. private final ConversationListener conversationListener = new ConversationListener() {
  643. @Override
  644. public void onNew(ConversationModel conversationModel) {}
  645. @Override
  646. public void onModified(ConversationModel modifiedConversationModel, Integer oldPosition) {}
  647. @Override
  648. public void onRemoved(ConversationModel conversationModel) {
  649. if (conversationModel != null) {
  650. boolean itsMyConversation = false;
  651. if (contactModel != null) {
  652. itsMyConversation = (conversationModel.getContact() != null
  653. && TestUtil.compare(conversationModel.getContact().getIdentity(), contactModel.getIdentity()));
  654. } else if (distributionListModel!= null) {
  655. itsMyConversation = conversationModel.getDistributionList() != null
  656. && TestUtil.compare(conversationModel.getDistributionList().getId(), distributionListModel.getId());
  657. } else if (groupModel != null) {
  658. itsMyConversation = conversationModel.getGroup() != null
  659. && TestUtil.compare(conversationModel.getGroup().getId(), groupModel.getId());
  660. }
  661. if (itsMyConversation) {
  662. RuntimeUtil.runOnUiThread(() -> {
  663. if (getActivity() != null) {
  664. getActivity().finish();
  665. }
  666. });
  667. }
  668. }
  669. }
  670. @Override
  671. public void onModifiedAll() {}
  672. };
  673. private final MessagePlayerListener messagePlayerListener = new MessagePlayerListener() {
  674. @Override
  675. public void onAudioStreamChanged(int newStreamType) { }
  676. @Override
  677. public void onAudioPlayEnded(AbstractMessageModel messageModel) {
  678. // Play next audio message, if any
  679. RuntimeUtil.runOnUiThread(() -> {
  680. if (composeMessageAdapter != null) {
  681. int index = composeMessageAdapter.getNextVoiceMessage(messageModel);
  682. if (index != AbsListView.INVALID_POSITION) {
  683. View view = composeMessageAdapter.getView(index, null, null);
  684. ComposeMessageHolder holder = (ComposeMessageHolder) view.getTag();
  685. if (holder.messagePlayer != null) {
  686. holder.messagePlayer.open();
  687. composeMessageAdapter.notifyDataSetChanged();
  688. }
  689. }
  690. }
  691. });
  692. }
  693. };
  694. private final QRCodeScanListener qrCodeScanListener = new QRCodeScanListener() {
  695. @Override
  696. public void onScanCompleted(String scanResult) {
  697. if (scanResult != null && scanResult.length() > 0) {
  698. RuntimeUtil.runOnUiThread(new Runnable() {
  699. @Override
  700. public void run() {
  701. if (messageText != null) {
  702. messageText.setText(scanResult);
  703. messageText.setSelection(messageText.length());
  704. }
  705. }
  706. });
  707. }
  708. }
  709. };
  710. private final BallotListener ballotListener = new BallotListener() {
  711. @Override
  712. public void onClosed(BallotModel ballotModel) { }
  713. @Override
  714. public void onModified(BallotModel ballotModel) { }
  715. @Override
  716. public void onCreated(BallotModel ballotModel) {
  717. try {
  718. BallotUtil.openDefaultActivity(
  719. getContext(),
  720. getFragmentManager(),
  721. ballotService.get(ballotModel.getId()),
  722. userService.getIdentity());
  723. } catch (Exception e) {
  724. logger.error("Exception", e);
  725. }
  726. }
  727. @Override
  728. public void onRemoved(BallotModel ballotModel) {
  729. }
  730. @Override
  731. public boolean handle(BallotModel ballotModel) {
  732. return ballotModel != null && userService.getIdentity().equals(ballotModel.getCreatorIdentity());
  733. }
  734. };
  735. @SuppressLint("StaticFieldLeak")
  736. @Override
  737. public void onRefresh() {
  738. logger.debug("onRefresh");
  739. if (actionMode != null || searchActionMode != null) {
  740. swipeRefreshLayout.setRefreshing(false);
  741. return;
  742. }
  743. new AsyncTask<Void, Void, Boolean>() {
  744. private List<AbstractMessageModel> messageModels;
  745. @Override
  746. protected Boolean doInBackground(Void... params) {
  747. messageModels = getNextRecords();
  748. if (messageModels != null) {
  749. return messageModels.size() >= nextMessageFilter.getPageSize();
  750. }
  751. return false;
  752. }
  753. @Override
  754. protected void onPostExecute(Boolean hasMoreRecords) {
  755. if (messageModels != null) {
  756. int numberOfInsertedRecords = insertToList(messageModels, false, true, true);
  757. if (numberOfInsertedRecords > 0) {
  758. convListView.setSelection(convListView.getSelectedItemPosition() + numberOfInsertedRecords + 1);
  759. }
  760. } else {
  761. composeMessageAdapter.notifyDataSetChanged();
  762. }
  763. // Notify PullToRefreshAttacher that the refresh has activity.finished
  764. swipeRefreshLayout.setRefreshing(false);
  765. swipeRefreshLayout.setEnabled(hasMoreRecords);
  766. }
  767. }.execute();
  768. }
  769. @Override
  770. public void onAttach(@NonNull Activity activity) {
  771. ((FragmentActivity) activity).getLifecycle().addObserver(this);
  772. logger.debug("onAttach");
  773. super.onAttach(activity);
  774. setHasOptionsMenu(true);
  775. this.activity = (ComposeMessageActivity) activity;
  776. this.audioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);
  777. if (bottomPanel != null) {
  778. bottomPanel.setVisibility(View.VISIBLE);
  779. }
  780. if (this.emojiPicker != null) {
  781. this.emojiPicker.init(activity);
  782. }
  783. // resolution and layout may have changed after being attached to a new activity
  784. ConfigUtils.getPreferredThumbnailWidth(activity, true);
  785. ConfigUtils.getPreferredAudioMessageWidth(activity, true);
  786. }
  787. @Override
  788. public void onCreate(Bundle savedInstanceState) {
  789. logger.debug("onCreate");
  790. if (getActivity() != null) {
  791. getActivity().supportPostponeEnterTransition();
  792. }
  793. super.onCreate(savedInstanceState);
  794. setRetainInstance(true);
  795. ListenerManager.contactTypingListeners.add(this.contactTypingListener);
  796. ListenerManager.messageListeners.add(this.messageListener, true);
  797. ListenerManager.groupListeners.add(this.groupListener);
  798. ListenerManager.contactListeners.add(this.contactListener);
  799. ListenerManager.conversationListeners.add(this.conversationListener);
  800. ListenerManager.messagePlayerListener.add(this.messagePlayerListener);
  801. ListenerManager.qrCodeScanListener.add(this.qrCodeScanListener);
  802. ListenerManager.ballotListeners.add(this.ballotListener);
  803. VoipListenerManager.callEventListener.add(this.voipCallEventListener);
  804. }
  805. @Override
  806. public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  807. logger.debug("onCreateView");
  808. if (!requiredInstances()) {
  809. activity.finish();
  810. return this.fragmentView;
  811. }
  812. this.layoutInflater = inflater;
  813. if (this.fragmentView == null) {
  814. // set font size
  815. activity.getTheme().applyStyle(preferenceService.getFontStyle(), true);
  816. this.fragmentView = inflater.inflate(R.layout.fragment_compose_message, container, false);
  817. ScrollView sv = fragmentView.findViewById(R.id.wallpaper_scroll);
  818. sv.setEnabled(false);
  819. sv.setOnTouchListener(null);
  820. sv.setOnClickListener(null);
  821. this.convListView = fragmentView.findViewById(R.id.history);
  822. ViewCompat.setNestedScrollingEnabled(this.convListView, true);
  823. this.convListView.setDivider(null);
  824. this.convListView.setClipToPadding(false);
  825. if (ConfigUtils.isTabletLayout()) {
  826. this.convListView.setPadding(0, 0, 0, 0);
  827. }
  828. this.listViewTop = this.convListView.getPaddingTop();
  829. this.swipeRefreshLayout = fragmentView.findViewById(R.id.ptr_layout);
  830. this.swipeRefreshLayout.setOnRefreshListener(this);
  831. this.swipeRefreshLayout.setColorSchemeResources(R.color.accent_light);
  832. this.swipeRefreshLayout.setSize(SwipeRefreshLayout.LARGE);
  833. this.coordinatorLayout = fragmentView.findViewById(R.id.coordinator);
  834. this.messageText = fragmentView.findViewById(R.id.embedded_text_editor);
  835. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
  836. // do not add on lollipop or lower due to this bug: https://issuetracker.google.com/issues/36937508
  837. this.messageText.setCustomSelectionActionModeCallback(textSelectionCallback);
  838. }
  839. this.sendButton = this.fragmentView.findViewById(R.id.send_button);
  840. this.attachButton = this.fragmentView.findViewById(R.id.attach_button);
  841. this.cameraButton = this.fragmentView.findViewById(R.id.camera_button);
  842. this.cameraButton.setOnClickListener(v -> {
  843. if (actionMode != null) {
  844. actionMode.finish();
  845. }
  846. closeQuoteMode();
  847. if (!validateSendingPermission()) {
  848. return;
  849. }
  850. if (ConfigUtils.requestCameraPermissions(activity, this, PERMISSION_REQUEST_ATTACH_CAMERA)) {
  851. attachCamera();
  852. }
  853. });
  854. updateCameraButton();
  855. this.emojiButton = this.fragmentView.findViewById(R.id.emoji_button);
  856. this.emojiButton.setOnClickListener(new View.OnClickListener() {
  857. @Override
  858. public void onClick(View v) {
  859. logger.info("Emoji button clicked");
  860. if (activity.isSoftKeyboardOpen()) {
  861. logger.info("Show emoji picker after keyboard close");
  862. activity.runOnSoftKeyboardClose(new Runnable() {
  863. @Override
  864. public void run() {
  865. if (emojiPicker != null) {
  866. emojiPicker.show(activity.loadStoredSoftKeyboardHeight());
  867. }
  868. }
  869. });
  870. messageText.post(new Runnable() {
  871. @Override
  872. public void run() {
  873. EditTextUtil.hideSoftKeyboard(messageText);
  874. }
  875. });
  876. } else {
  877. if (emojiPicker != null) {
  878. if (emojiPicker.isShown()) {
  879. logger.info("EmojPicker currently shown. Closing.");
  880. if (ConfigUtils.isLandscape(activity) &&
  881. !ConfigUtils.isTabletLayout() &&
  882. preferenceService.isFullscreenIme()) {
  883. emojiPicker.hide();
  884. } else {
  885. activity.openSoftKeyboard(emojiPicker, messageText);
  886. if (activity.getResources().getConfiguration().keyboard == Configuration.KEYBOARD_QWERTY) {
  887. emojiPicker.hide();
  888. }
  889. }
  890. } else {
  891. logger.info("Show emoji picker immediately");
  892. emojiPicker.show(activity.loadStoredSoftKeyboardHeight());
  893. }
  894. }
  895. }
  896. }
  897. });
  898. this.emojiMarkupUtil = EmojiMarkupUtil.getInstance();
  899. this.wallpaperView = this.fragmentView.findViewById(R.id.wallpaper_view);
  900. this.quickscrollUpView = this.fragmentView.findViewById(R.id.quickscroll_top);
  901. this.quickscrollDownView = this.fragmentView.findViewById(R.id.quickscroll_bottom);
  902. this.dateView = this.fragmentView.findViewById(R.id.date_separator_container);
  903. this.dateTextView = this.fragmentView.findViewById(R.id.text_view);
  904. quoteInfo.quotePanel = this.fragmentView.findViewById(R.id.quote_panel);
  905. quoteInfo.quoteTextView = this.fragmentView.findViewById(R.id.quote_text_view);
  906. quoteInfo.quoteIdentityView = this.fragmentView.findViewById(R.id.quote_id_view);
  907. quoteInfo.quoteBar = this.fragmentView.findViewById(R.id.quote_bar);
  908. quoteInfo.quoteThumbnail = this.fragmentView.findViewById(R.id.quote_thumbnail);
  909. quoteInfo.quoteTypeImage = this.fragmentView.findViewById(R.id.quote_type_image);
  910. ImageView quoteCloseButton = this.fragmentView.findViewById(R.id.quote_panel_close_button);
  911. quoteCloseButton.setOnClickListener(v -> closeQuoteMode());
  912. this.bottomPanel = this.fragmentView.findViewById(R.id.bottom_panel);
  913. this.openBallotNoticeView = this.fragmentView.findViewById(R.id.open_ballots_layout);
  914. this.openGroupRequestNoticeView = this.fragmentView.findViewById(R.id.open_group_requests_layout);
  915. this.getValuesFromBundle(savedInstanceState);
  916. this.handleIntent(activity.getIntent());
  917. this.setupListeners();
  918. }
  919. if (preferenceService.getEmojiStyle() == PreferenceService.EmojiStyle_ANDROID) {
  920. // remove emoji button
  921. this.emojiButton.setVisibility(View.GONE);
  922. this.messageText.setPadding(getResources().getDimensionPixelSize(R.dimen.no_emoji_button_padding_left), this.messageText.getPaddingTop(), this.messageText.getPaddingRight(), this.messageText.getPaddingBottom());
  923. } else {
  924. try {
  925. this.emojiPicker = (EmojiPicker) ((ViewStub) this.activity.findViewById(R.id.emoji_stub)).inflate();
  926. this.emojiPicker.init(activity);
  927. this.emojiButton.attach(this.emojiPicker, preferenceService.isFullscreenIme());
  928. this.emojiPicker.setEmojiKeyListener(new EmojiPicker.EmojiKeyListener() {
  929. @Override
  930. public void onBackspaceClick() {
  931. messageText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
  932. }
  933. @Override
  934. public void onEmojiClick(String emojiCodeString) {
  935. RuntimeUtil.runOnUiThread(() -> messageText.addEmoji(emojiCodeString));
  936. }
  937. });
  938. this.emojiPicker.addEmojiPickerListener(this);
  939. } catch (Exception e) {
  940. logger.error("Exception", e);
  941. activity.finish();
  942. }
  943. }
  944. return this.fragmentView;
  945. }
  946. private final android.view.ActionMode.Callback textSelectionCallback = new android.view.ActionMode.Callback() {
  947. private final Pattern pattern = Pattern.compile("\\B");
  948. @Override
  949. public boolean onCreateActionMode(android.view.ActionMode mode, Menu menu) {
  950. return true;
  951. }
  952. @Override
  953. public boolean onPrepareActionMode(android.view.ActionMode mode, Menu menu) {
  954. menu.removeGroup(CONTEXT_MENU_GROUP);
  955. if (messageText != null && messageText.getText() != null) {
  956. String text = messageText.getText().toString();
  957. if (text.length() > 1) {
  958. int start = messageText.getSelectionStart();
  959. int end = messageText.getSelectionEnd();
  960. try {
  961. if ((start <= 0 || pattern.matcher(text.substring(start - 1, start)).find()) &&
  962. (end >= text.length() || pattern.matcher(text.substring(end, end + 1)).find()) &&
  963. !text.substring(start, end).contains("\n")) {
  964. menu.add(CONTEXT_MENU_GROUP, CONTEXT_MENU_BOLD, 200, R.string.bold);
  965. menu.add(CONTEXT_MENU_GROUP, CONTEXT_MENU_ITALIC, 201, R.string.italic);
  966. menu.add(CONTEXT_MENU_GROUP, CONTEXT_MENU_STRIKETHRU, 203, R.string.strikethrough);
  967. }
  968. } catch (StringIndexOutOfBoundsException e) {
  969. // do not add menus
  970. }
  971. }
  972. }
  973. return true;
  974. }
  975. @Override
  976. public boolean onActionItemClicked(android.view.ActionMode mode, MenuItem item) {
  977. switch (item.getItemId()) {
  978. case CONTEXT_MENU_BOLD:
  979. addMarkup("*");
  980. break;
  981. case CONTEXT_MENU_ITALIC:
  982. addMarkup("_");
  983. break;
  984. case CONTEXT_MENU_STRIKETHRU:
  985. addMarkup("~");
  986. break;
  987. default:
  988. return false;
  989. }
  990. return true;
  991. }
  992. @Override
  993. public void onDestroyActionMode(android.view.ActionMode mode) {
  994. }
  995. };
  996. private void addMarkup(String string) {
  997. Editable editable = messageText.getText();
  998. if (editable.length() > 0) {
  999. int start = messageText.getSelectionStart();
  1000. int end = messageText.getSelectionEnd();
  1001. editable.insert(end, string);
  1002. editable.insert(start, string);
  1003. }
  1004. messageText.invalidate();
  1005. }
  1006. @Override
  1007. public void onActivityCreated(Bundle savedInstanceState) {
  1008. logger.debug("onActivityCreated");
  1009. super.onActivityCreated(savedInstanceState);
  1010. /*
  1011. * This callback tells the fragment when it is fully associated with the new activity instance. This is called after onCreateView(LayoutInflater, ViewGroup, Bundle) and before onViewStateRestored(Bundle).
  1012. */
  1013. activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
  1014. if (preferenceService == null) {
  1015. return;
  1016. }
  1017. if (preferenceService.getEmojiStyle() != PreferenceService.EmojiStyle_ANDROID) {
  1018. try {
  1019. View rootView = activity.getWindow().getDecorView().getRootView();
  1020. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
  1021. try {
  1022. ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
  1023. if (decorView.getChildCount() == 1 && decorView.getChildAt(0) instanceof LinearLayout) {
  1024. rootView = decorView.getChildAt(0);
  1025. }
  1026. } catch (Exception e) {
  1027. logger.error("Exception", e);
  1028. }
  1029. }
  1030. ViewCompat.setOnApplyWindowInsetsListener(rootView, new OnApplyWindowInsetsListener() {
  1031. @Override
  1032. public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
  1033. logger.debug("system window top " + insets.getSystemWindowInsetTop() + " bottom " + insets.getSystemWindowInsetBottom());
  1034. logger.debug("stable insets top " + insets.getStableInsetTop() + " bottom " + insets.getStableInsetBottom());
  1035. if (insets.getSystemWindowInsetBottom() <= insets.getStableInsetBottom()) {
  1036. activity.onSoftKeyboardClosed();
  1037. } else {
  1038. activity.onSoftKeyboardOpened(insets.getSystemWindowInsetBottom() - insets.getStableInsetBottom());
  1039. }
  1040. return insets;
  1041. }
  1042. });
  1043. } catch (NullPointerException e) {
  1044. logger.error("Exception", e);
  1045. }
  1046. activity.addOnSoftKeyboardChangedListener(this);
  1047. }
  1048. // restore action mode after rotate if the activity was detached
  1049. if (convListView != null && convListView.getCheckedItemCount() > 0 && actionMode != null) {
  1050. actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(new ComposeMessageAction(this.longClickItem));
  1051. }
  1052. }
  1053. @Override
  1054. public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
  1055. super.onViewCreated(view, savedInstanceState);
  1056. activity.supportStartPostponedEnterTransition();
  1057. }
  1058. @Override
  1059. public void onResume(@NonNull LifecycleOwner owner) {
  1060. logger.debug("onResume");
  1061. //set visible receiver
  1062. if (this.messageReceiver != null) {
  1063. this.notificationService.setVisibleReceiver(this.messageReceiver);
  1064. isPaused = false;
  1065. logger.debug("markAllRead");
  1066. // mark all unread messages
  1067. if (this.unreadMessages.size() > 0) {
  1068. ReadMessagesRoutine r = new ReadMessagesRoutine(this.unreadMessages,
  1069. this.messageService,
  1070. this.notificationService);
  1071. r.addOnFinished(new ReadMessagesRoutine.OnFinished() {
  1072. @Override
  1073. public void finished(boolean success) {
  1074. if (success) {
  1075. unreadMessages.clear();
  1076. }
  1077. }
  1078. });
  1079. new Thread(r).start();
  1080. }
  1081. // update menus
  1082. updateMuteMenu();
  1083. // start media players again
  1084. this.messagePlayerService.resumeAll(getActivity(), this.messageReceiver, SOURCE_LIFECYCLE);
  1085. // make sure to remark the active chat
  1086. if (ConfigUtils.isTabletLayout()) {
  1087. ListenerManager.chatListener.handle(listener -> listener.onChatOpened(this.conversationUid));
  1088. }
  1089. // restore scroll position after orientation change
  1090. if (getActivity() != null) {
  1091. Intent intent = getActivity().getIntent();
  1092. if (intent != null && !intent.hasExtra(EXTRA_API_MESSAGE_ID) && !intent.hasExtra(EXTRA_SEARCH_QUERY)) {
  1093. convListView.post(new Runnable() {
  1094. @Override
  1095. public void run() {
  1096. if (listInstancePosition != AbsListView.INVALID_POSITION &&
  1097. messageReceiver != null &&
  1098. messageReceiver.getUniqueIdString().equals(listInstanceReceiverId)) {
  1099. logger.debug("restoring position " + listInstancePosition);
  1100. convListView.setSelectionFromTop(listInstancePosition, listInstanceTop);
  1101. } else {
  1102. jumpToFirstUnreadMessage();
  1103. }
  1104. // make sure it's not restored twice
  1105. listInstancePosition = AbsListView.INVALID_POSITION;
  1106. listInstanceReceiverId = null;
  1107. }
  1108. });
  1109. }
  1110. }
  1111. // update group requests as they could have been changed when coming back from the overview activity
  1112. if (ConfigUtils.supportsGroupLinks()) {
  1113. this.openGroupRequestNoticeView.updateGroupRequests();
  1114. }
  1115. }
  1116. }
  1117. @Override
  1118. public void onPause(@NonNull LifecycleOwner owner) {
  1119. logger.debug("onPause");
  1120. isPaused = true;
  1121. onEmojiPickerClose();
  1122. if (this.notificationService != null) {
  1123. this.notificationService.setVisibleReceiver(null);
  1124. }
  1125. //stop all playing audio messages (incoming call?)
  1126. if (this.messagePlayerService != null) {
  1127. this.messagePlayerService.pauseAll(SOURCE_LIFECYCLE);
  1128. }
  1129. // save unfinished text
  1130. saveMessageDraft();
  1131. if(this.typingIndicatorTextWatcher != null) {
  1132. this.typingIndicatorTextWatcher.stopTyping();
  1133. }
  1134. preserveListInstanceValues();
  1135. }
  1136. @Override
  1137. public void onStop() {
  1138. logger.debug("onStop");
  1139. if(this.typingIndicatorTextWatcher != null) {
  1140. this.typingIndicatorTextWatcher.stopTyping();
  1141. }
  1142. if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
  1143. // close keyboard to prevent layout corruption after unlocking phone
  1144. if (this.messageText != null) {
  1145. EditTextUtil.hideSoftKeyboard(this.messageText);
  1146. }
  1147. }
  1148. super.onStop();
  1149. }
  1150. @Override
  1151. public void onDetach() {
  1152. logger.debug("onDetach");
  1153. if (this.emojiPicker != null && this.emojiPicker.isShown()) {
  1154. this.emojiPicker.hide();
  1155. }
  1156. dismissMentionPopup();
  1157. this.activity = null;
  1158. super.onDetach();
  1159. }
  1160. @Override
  1161. public void onDestroy() {
  1162. logger.debug("onDestroy");
  1163. try {
  1164. ListenerManager.contactTypingListeners.remove(this.contactTypingListener);
  1165. ListenerManager.groupListeners.remove(this.groupListener);
  1166. ListenerManager.messageListeners.remove(this.messageListener);
  1167. ListenerManager.contactListeners.remove(this.contactListener);
  1168. ListenerManager.conversationListeners.remove(this.conversationListener);
  1169. ListenerManager.messagePlayerListener.remove(this.messagePlayerListener);
  1170. ListenerManager.qrCodeScanListener.remove(this.qrCodeScanListener);
  1171. ListenerManager.ballotListeners.remove(this.ballotListener);
  1172. VoipListenerManager.callEventListener.remove(this.voipCallEventListener);
  1173. dismissTooltipPopup(workTooltipPopup, true);
  1174. workTooltipPopup = null;
  1175. dismissMentionPopup();
  1176. if (this.emojiButton != null) {
  1177. this.emojiButton.detach(this.emojiPicker);
  1178. }
  1179. if (this.emojiPicker != null) {
  1180. this.emojiPicker.removeEmojiPickerListener(this);
  1181. }
  1182. if (!requiredInstances()) {
  1183. super.onDestroy();
  1184. return;
  1185. }
  1186. //release all players!
  1187. if (this.messagePlayerService != null) {
  1188. this.messagePlayerService.release();
  1189. }
  1190. if (this.messageService != null) {
  1191. this.messageService.saveMessageQueueAsync();
  1192. }
  1193. if (this.thumbnailCache != null) {
  1194. this.thumbnailCache.flush();
  1195. }
  1196. if (this.messageText != null) {
  1197. //remove typing change listener
  1198. if(this.typingIndicatorTextWatcher != null) {
  1199. this.messageText.removeTextChangedListener(this.typingIndicatorTextWatcher);
  1200. }
  1201. // http://stackoverflow.com/questions/18348049/android-edittext-memory-leak
  1202. this.messageText.setText(null);
  1203. }
  1204. //remove wallpaper
  1205. this.wallpaperView.setImageBitmap(null);
  1206. // delete pending deleteable messages
  1207. deleteDeleteableMessages();
  1208. if (this.deleteSnackbar != null && this.deleteSnackbar.isShownOrQueued()) {
  1209. this.deleteSnackbar.dismiss();
  1210. }
  1211. removeIsTypingFooter();
  1212. this.isTypingView = null;
  1213. //clear all records to remove all references
  1214. if(this.composeMessageAdapter != null) {
  1215. this.composeMessageAdapter.clear();
  1216. this.composeMessageAdapter = null;
  1217. }
  1218. } catch (Exception x) {
  1219. logger.error("Exception", x);
  1220. }
  1221. super.onDestroy();
  1222. }
  1223. private void removeScrollButtons() {
  1224. logger.debug("removeScrollButtons");
  1225. if (dateView != null && dateView.getVisibility() == View.VISIBLE) {
  1226. AnimationUtil.slideOutAnimation(dateView, false, 1f, null);
  1227. }
  1228. if (actionMode != null) {
  1229. actionMode.finish();
  1230. }
  1231. }
  1232. private void setupListeners() {
  1233. // Setting this scroll listener is required to ensure that during ListView scrolling,
  1234. // we don't look for swipes or pulldowns
  1235. this.convListView.setOnScrollListener(new AbsListView.OnScrollListener() {
  1236. @Override
  1237. public void onScrollStateChanged(AbsListView absListView, int scrollState) {
  1238. if (listViewSwipeListener != null) {
  1239. listViewSwipeListener.setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
  1240. }
  1241. if (!absListView.canScrollList(View.SCROLL_AXIS_VERTICAL)) {
  1242. AnimationUtil.setFadingVisibility(quickscrollDownView, View.GONE);
  1243. }
  1244. if (!absListView.canScrollList(-View.SCROLL_AXIS_VERTICAL)) {
  1245. AnimationUtil.setFadingVisibility(quickscrollUpView, View.GONE);
  1246. }
  1247. }
  1248. @Override
  1249. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  1250. if(view != null && view.getChildCount() > 0) {
  1251. View itemView = view.getChildAt(0);
  1252. boolean onTop = firstVisibleItem == 0 && itemView.getTop() == listViewTop;
  1253. swipeRefreshLayout.setEnabled(onTop);
  1254. if (firstVisibleItem != lastFirstVisibleItem) {
  1255. if (lastFirstVisibleItem < firstVisibleItem) {
  1256. // scrolling down
  1257. AnimationUtil.setFadingVisibility(quickscrollUpView, View.GONE);
  1258. if (view.canScrollList(View.SCROLL_AXIS_VERTICAL)) {
  1259. AnimationUtil.setFadingVisibility(quickscrollDownView, View.VISIBLE);
  1260. }
  1261. } else {
  1262. // scrolling up
  1263. AnimationUtil.setFadingVisibility(quickscrollDownView, View.GONE);
  1264. if (view.canScrollList(-View.SCROLL_AXIS_VERTICAL)) {
  1265. AnimationUtil.setFadingVisibility(quickscrollUpView, View.VISIBLE);
  1266. }
  1267. }
  1268. lastFirstVisibleItem = firstVisibleItem;
  1269. if (dateView.getVisibility() != View.VISIBLE && composeMessageAdapter != null && composeMessageAdapter.getCount() > 0) {
  1270. AnimationUtil.slideInAnimation(dateView, false, 200);
  1271. }
  1272. dateViewHandler.removeCallbacks(dateViewTask);
  1273. dateViewHandler.postDelayed(dateViewTask, SCROLLBUTTON_VIEW_TIMEOUT);
  1274. if (composeMessageAdapter != null) {
  1275. AbstractMessageModel abstractMessageModel = composeMessageAdapter.getItem(firstVisibleItem);
  1276. if (abstractMessageModel != null) {
  1277. final Date createdAt = abstractMessageModel.getCreatedAt();
  1278. if (createdAt != null) {
  1279. final String text = LocaleUtil.formatDateRelative(getActivity(), createdAt.getTime());
  1280. dateTextView.setText(text);
  1281. dateView.post(() -> {
  1282. dateTextView.setText(text);
  1283. });
  1284. }
  1285. }
  1286. }
  1287. }
  1288. }
  1289. else {
  1290. swipeRefreshLayout.setEnabled(false);
  1291. }
  1292. }
  1293. });
  1294. listViewSwipeListener = new ListViewSwipeListener(
  1295. this.convListView,
  1296. new ListViewSwipeListener.DismissCallbacks() {
  1297. @Override
  1298. public boolean canSwipe(int position) {
  1299. if (actionMode != null) {
  1300. return false;
  1301. }
  1302. if (messageReceiver == null || !messageReceiver.validateSendingPermission(null)) {
  1303. return false;
  1304. }
  1305. int viewType = composeMessageAdapter.getItemViewType(position);
  1306. if (viewType == ComposeMessageAdapter.TYPE_STATUS ||
  1307. viewType == ComposeMessageAdapter.TYPE_FIRST_UNREAD ||
  1308. viewType == ComposeMessageAdapter.TYPE_DATE_SEPARATOR) {
  1309. return false;
  1310. }
  1311. AbstractMessageModel messageModel = composeMessageAdapter.getItem(position);
  1312. if (messageModel == null) {
  1313. return false;
  1314. }
  1315. return QuoteUtil.isQuoteable(messageModel);
  1316. }
  1317. @Override
  1318. public void onSwiped(int position) {
  1319. if (composeMessageAdapter == null) {
  1320. return;
  1321. }
  1322. AbstractMessageModel abstractMessageModel = composeMessageAdapter.getItem(position);
  1323. if (preferenceService.isInAppVibrate()) {
  1324. if (isAdded() && !isDetached() && activity != null) {
  1325. Vibrator vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
  1326. if (vibrator != null && vibrator.hasVibrator()) {
  1327. vibrator.vibrate(100);
  1328. }
  1329. }
  1330. }
  1331. if (abstractMessageModel != null) {
  1332. if (isQuotePanelShown() && abstractMessageModel.equals(quoteInfo.messageModel)) {
  1333. closeQuoteMode();
  1334. } else {
  1335. startQuoteMode(abstractMessageModel, new Runnable() {
  1336. @Override
  1337. public void run() {
  1338. RuntimeUtil.runOnUiThread(new Runnable() {
  1339. @Override
  1340. public void run() {
  1341. EditTextUtil.showSoftKeyboard(messageText);
  1342. }
  1343. });
  1344. }
  1345. });
  1346. }
  1347. }
  1348. }
  1349. }
  1350. );
  1351. this.quickscrollDownView.setOnClickListener(new View.OnClickListener() {
  1352. @Override
  1353. public void onClick(View v) {
  1354. removeScrollButtons();
  1355. scrollList(Integer.MAX_VALUE);
  1356. }
  1357. });
  1358. this.quickscrollUpView.setOnClickListener(new View.OnClickListener() {
  1359. @Override
  1360. public void onClick(View v) {
  1361. removeScrollButtons();
  1362. scrollList(0);
  1363. }
  1364. });
  1365. if (sendButton != null) {
  1366. sendButton.setOnClickListener(new DebouncedOnClickListener(500) {
  1367. @Override
  1368. public void onDebouncedClick(View v) {
  1369. sendMessage();
  1370. }
  1371. });
  1372. }
  1373. if (attachButton != null) {
  1374. attachButton.setOnClickListener(new DebouncedOnClickListener(1000) {
  1375. @Override
  1376. public void onDebouncedClick(View v) {
  1377. if (validateSendingPermission()) {
  1378. if (actionMode != null) {
  1379. actionMode.finish();
  1380. }
  1381. closeQuoteMode();
  1382. Intent intent = new Intent(activity, MediaAttachActivity.class);
  1383. IntentDataUtil.addMessageReceiverToIntent(intent, messageReceiver);
  1384. if (ComposeMessageFragment.this.lastMediaFilter != null) {
  1385. intent = IntentDataUtil.addLastMediaFilterToIntent(intent, ComposeMessageFragment.this.lastMediaFilter);
  1386. }
  1387. activity.startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_ATTACH_MEDIA);
  1388. activity.overridePendingTransition(R.anim.fast_fade_in, R.anim.fast_fade_out);
  1389. }
  1390. }
  1391. });
  1392. }
  1393. this.messageText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
  1394. @Override
  1395. public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
  1396. if ((actionId == EditorInfo.IME_ACTION_SEND) ||
  1397. (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && preferenceService.isEnterToSend())) {
  1398. sendMessage();
  1399. return true;
  1400. }
  1401. return false;
  1402. }
  1403. });
  1404. if (ConfigUtils.isDefaultEmojiStyle()) {
  1405. this.messageText.setOnClickListener(new View.OnClickListener() {
  1406. @Override
  1407. public void onClick(View v) {
  1408. if (emojiPicker != null) {
  1409. if (emojiPicker.isShown()) {
  1410. if (ConfigUtils.isLandscape(activity) &&
  1411. !ConfigUtils.isTabletLayout() &&
  1412. preferenceService.isFullscreenIme()) {
  1413. emojiPicker.hide();
  1414. } else {
  1415. activity.openSoftKeyboard(emojiPicker, messageText);
  1416. }
  1417. }
  1418. }
  1419. }
  1420. });
  1421. }
  1422. this.messageText.addTextChangedListener(new TextWatcher() {
  1423. @Override
  1424. public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  1425. }
  1426. @Override
  1427. public void onTextChanged(CharSequence s, int start, int before, int count) {
  1428. ThreemaApplication.activityUserInteract(activity);
  1429. updateSendButton(s);
  1430. if (isGroupChat && s.length() > 0 && getActivity() != null && getActivity().getCurrentFocus() == messageText) {
  1431. checkPossibleMention(s, start, before, count);
  1432. }
  1433. }
  1434. @Override
  1435. public void afterTextChanged(Editable s) {
  1436. updateCameraButton();
  1437. }
  1438. });
  1439. }
  1440. private void updateCameraButton() {
  1441. if (cameraButton == null || messageText == null) {
  1442. return;
  1443. }
  1444. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
  1445. ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
  1446. // shouldShowRequestPermissionRationale returns false if
  1447. // a) the user selected "never ask again"; or
  1448. // b) a permission dialog has never been shown
  1449. // we hide the camera button only in case a)
  1450. if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) && preferenceService.getCameraPermissionRequestShown()) {
  1451. cameraButton.setVisibility(View.GONE);
  1452. fixMessageTextPadding(View.GONE);
  1453. return;
  1454. }
  1455. }
  1456. int visibility = messageText.getText() == null ||
  1457. messageText.getText().length() == 0 ?
  1458. View.VISIBLE : View.GONE;
  1459. if (cameraButton.getVisibility() != visibility) {
  1460. Transition transition = new Slide(Gravity.RIGHT);
  1461. transition.setDuration(150);
  1462. transition.setInterpolator(new LinearInterpolator());
  1463. transition.addTarget(R.id.camera_button);
  1464. TransitionManager.beginDelayedTransition((ViewGroup) cameraButton.getParent(), transition);
  1465. cameraButton.setVisibility(visibility);
  1466. fixMessageTextPadding(visibility);
  1467. }
  1468. }
  1469. private void fixMessageTextPadding(int visibility) {
  1470. int marginRight = getResources().getDimensionPixelSize(visibility == View.VISIBLE ? R.dimen.emoji_and_photo_button_width : R.dimen.emoji_button_width);
  1471. messageText.setPadding(messageText.getPaddingLeft(), messageText.getPaddingTop(), marginRight, messageText.getPaddingBottom());
  1472. }
  1473. private void checkPossibleMention(CharSequence s, int start, int before, int count) {
  1474. if (count == 1 && s.charAt(start) == '@' && start < s.length() // if current char is @ and only if adding a new char
  1475. && (start == 0 || s.charAt(start - 1) == ' ' || s.charAt(start - 1) == '\n') // and only show if at start or if there is empty space before to not interrupt typing mail addresses
  1476. && (s.length() <= start + 1 || s.charAt(start + 1) == ' ' || s.charAt(start + 1) == '\n') // and only show if @ is at the very end or also has empty space in the back.
  1477. )
  1478. {
  1479. dismissTooltipPopup(workTooltipPopup, true);
  1480. workTooltipPopup = null;
  1481. dismissMentionPopup();
  1482. mentionPopup = new MentionSelectorPopup(getActivity(), this, groupService, this.contactService, this.userService, this.preferenceService, groupModel);
  1483. mentionPopup.show(getActivity(), this.messageText, emojiButton.getWidth());
  1484. }
  1485. }
  1486. private void updateSendButton(CharSequence s) {
  1487. if (isQuotePanelShown()) {
  1488. if (TestUtil.empty(s)) {
  1489. sendButton.setEnabled(false);
  1490. } else {
  1491. sendButton.setSend();
  1492. sendButton.setEnabled(true);
  1493. }
  1494. } else {
  1495. if (TestUtil.empty(s)) {
  1496. sendButton.setRecord();
  1497. sendButton.setEnabled(true);
  1498. } else {
  1499. sendButton.setSend();
  1500. sendButton.setEnabled(true);
  1501. }
  1502. }
  1503. if (emojiButton != null) emojiButton.setVisibility(preferenceService.getEmojiStyle() != PreferenceService.EmojiStyle_ANDROID ? View.VISIBLE : View.GONE);
  1504. if (messageText != null) messageText.setVisibility(View.VISIBLE);
  1505. }
  1506. private void setBackgroundWallpaper() {
  1507. if (isAdded() && this.wallpaperView != null) {
  1508. wallpaperService.setupWallpaperBitmap(this.messageReceiver, this.wallpaperView, ConfigUtils.isLandscape(activity));
  1509. }
  1510. }
  1511. private void resetDefaultValues() {
  1512. this.distributionListId = 0;
  1513. this.groupId = 0;
  1514. this.identity = null;
  1515. this.groupModel = null;
  1516. this.distributionListModel = null;
  1517. this.contactModel = null;
  1518. this.messageReceiver = null;
  1519. this.listInstancePosition = AbsListView.INVALID_POSITION;
  1520. this.listInstanceReceiverId = null;
  1521. if (ConfigUtils.isTabletLayout()) {
  1522. // apply pending deletes upon reentering a chat through onNewIntent() in multi-frame environment
  1523. deleteDeleteableMessages();
  1524. if (this.deleteSnackbar != null && this.deleteSnackbar.isShownOrQueued()) {
  1525. this.deleteSnackbar.dismiss();
  1526. }
  1527. closeQuoteMode();
  1528. }
  1529. }
  1530. private void getValuesFromBundle(Bundle bundle) {
  1531. if (bundle != null) {
  1532. this.groupId = bundle.getInt(ThreemaApplication.INTENT_DATA_GROUP, 0);
  1533. this.distributionListId = bundle.getInt(ThreemaApplication.INTENT_DATA_DISTRIBUTION_LIST, 0);
  1534. this.identity = bundle.getString(ThreemaApplication.INTENT_DATA_CONTACT);
  1535. this.intentTimestamp = bundle.getLong(ThreemaApplication.INTENT_DATA_TIMESTAMP, 0L);
  1536. this.cameraUri = bundle.getParcelable(CAMERA_URI);
  1537. }
  1538. }
  1539. public void onNewIntent(Intent intent) {
  1540. logger.debug("onNewIntent");
  1541. if (!requiredInstances()) {
  1542. return;
  1543. }
  1544. resetDefaultValues();
  1545. handleIntent(intent);
  1546. // initialize various toolbar items
  1547. if (actionMode != null) {
  1548. actionMode.finish();
  1549. }
  1550. if (searchActionMode != null) {
  1551. searchActionMode.finish();
  1552. }
  1553. this.closeQuoteMode();
  1554. this.updateToolbarTitle();
  1555. this.updateMenus();
  1556. }
  1557. private void setupToolbar() {
  1558. View actionBarTitleView = layoutInflater.inflate(R.layout.actionbar_compose_title, null);
  1559. if (actionBarTitleView != null) {
  1560. this.actionBarTitleTextView = actionBarTitleView.findViewById(R.id.title);
  1561. this.actionBarSubtitleImageView = actionBarTitleView.findViewById(R.id.subtitle_image);
  1562. this.actionBarSubtitleTextView = actionBarTitleView.findViewById(R.id.subtitle_text);
  1563. this.actionBarAvatarView = actionBarTitleView.findViewById(R.id.avatar_view);
  1564. final RelativeLayout actionBarTitleContainer = actionBarTitleView.findViewById(R.id.title_container);
  1565. actionBarTitleContainer.setOnClickListener(new View.OnClickListener() {
  1566. @Override
  1567. public void onClick(View v) {
  1568. Intent intent = null;
  1569. if (isGroupChat) {
  1570. if (groupService.isGroupMember(groupModel)) {
  1571. intent = groupService.getGroupEditIntent(groupModel, activity);
  1572. }
  1573. } else if (isDistributionListChat) {
  1574. intent = new Intent(activity, DistributionListAddActivity.class);
  1575. } else {
  1576. intent = new Intent(activity, ContactDetailActivity.class);
  1577. intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT_READONLY, true);
  1578. }
  1579. if (intent != null) {
  1580. intent = addExtrasToIntent(intent, messageReceiver);
  1581. activity.startActivityForResult(intent, 0);
  1582. }
  1583. }
  1584. });
  1585. if (contactModel != null) {
  1586. if (contactModel.getIdentityType() == IdentityType.WORK) {
  1587. if (!ConfigUtils.isWorkBuild()) {
  1588. if (!preferenceService.getIsWorkHintTooltipShown()) {
  1589. actionBarTitleTextView.postDelayed(() -> {
  1590. if (getActivity() != null && isAdded()) {
  1591. dismissTooltipPopup(workTooltipPopup, true);
  1592. int[] location = new int[2];
  1593. actionBarAvatarView.getLocationOnScreen(location);
  1594. location[0] += actionBarAvatarView.getWidth() / 2;
  1595. location[1] += actionBarAvatarView.getHeight();
  1596. workTooltipPopup = new TooltipPopup(getActivity(), R.string.preferences__tooltip_work_hint_shown, R.layout.popup_tooltip_top_left_work, this, new Intent(getActivity(), WorkExplainActivity.class));
  1597. workTooltipPopup.show(getActivity(), actionBarAvatarView, getString(R.string.tooltip_work_hint), TooltipPopup.ALIGN_BELOW_ANCHOR_ARROW_LEFT, location, 4000);
  1598. }
  1599. }, 1000);
  1600. }
  1601. }
  1602. }
  1603. }
  1604. }
  1605. if (activity == null) {
  1606. activity = (ComposeMessageActivity) getActivity();
  1607. }
  1608. if (activity != null) {
  1609. this.actionBar = activity.getSupportActionBar();
  1610. if (actionBar != null) {
  1611. actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_HOME_AS_UP);
  1612. actionBar.setCustomView(actionBarTitleView);
  1613. }
  1614. }
  1615. }
  1616. @UiThread
  1617. private void handleIntent(Intent intent) {
  1618. logger.debug("handleIntent");
  1619. this.isGroupChat = false;
  1620. this.isDistributionListChat = false;
  1621. this.currentPageReferenceId = null;
  1622. //remove old indicator every time!
  1623. //fix ANDR-432
  1624. if (this.typingIndicatorTextWatcher != null) {
  1625. if (this.messageText != null) {
  1626. this.messageText.removeTextChangedListener(this.typingIndicatorTextWatcher);
  1627. }
  1628. }
  1629. if (intent.hasExtra(ThreemaApplication.INTENT_DATA_GROUP) || this.groupId != 0) {
  1630. this.isGroupChat = true;
  1631. if (this.groupId == 0) {
  1632. this.groupId = intent.getIntExtra(ThreemaApplication.INTENT_DATA_GROUP, 0);
  1633. }
  1634. this.groupModel = this.groupService.getById(this.groupId);
  1635. if (this.groupModel == null || this.groupModel.isDeleted()) {
  1636. logger.error(activity.getString(R.string.group_not_found), activity, new Runnable() {
  1637. @Override
  1638. public void run() {
  1639. activity.finish();
  1640. }
  1641. });
  1642. return;
  1643. }
  1644. intent.removeExtra(ThreemaApplication.INTENT_DATA_GROUP);
  1645. this.messageReceiver = this.groupService.createReceiver(this.groupModel);
  1646. this.conversationUid = ConversationUtil.getGroupConversationUid(this.groupId);
  1647. if (ConfigUtils.supportsGroupLinks() && groupService.isGroupOwner(this.groupModel)) {
  1648. this.openGroupRequestNoticeView.setGroupIdReference(this.groupId);
  1649. this.openGroupRequestNoticeView.updateGroupRequests();
  1650. }
  1651. } else if (intent.hasExtra(ThreemaApplication.INTENT_DATA_DISTRIBUTION_LIST) || this.distributionListId != 0) {
  1652. this.isDistributionListChat = true;
  1653. try {
  1654. if (this.distributionListId == 0) {
  1655. this.distributionListId = intent.getIntExtra(ThreemaApplication.INTENT_DATA_DISTRIBUTION_LIST, 0);
  1656. }
  1657. this.distributionListModel = distributionListService.getById(this.distributionListId);
  1658. if (this.distributionListModel == null) {
  1659. logger.error("Invalid distribution list", activity, new Runnable() {
  1660. @Override
  1661. public void run() {
  1662. activity.finish();
  1663. }
  1664. });
  1665. return;
  1666. }
  1667. intent.removeExtra(ThreemaApplication.INTENT_DATA_DISTRIBUTION_LIST);
  1668. this.messageReceiver = distributionListService.createReceiver(this.distributionListModel);
  1669. } catch (Exception e) {
  1670. logger.error("Exception", e);
  1671. return;
  1672. }
  1673. this.conversationUid = ConversationUtil.getDistributionListConversationUid(this.distributionListId);
  1674. } else {
  1675. if (TestUtil.empty(this.identity)) {
  1676. this.identity = intent.getStringExtra(ThreemaApplication.INTENT_DATA_CONTACT);
  1677. }
  1678. if (this.identity == null) {
  1679. if (intent.getData() != null) {
  1680. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
  1681. this.identity = ContactUtil.getIdentityFromViewIntent(activity, intent);
  1682. } else {
  1683. Toast.makeText(activity, R.string.permission_contacts_required, Toast.LENGTH_LONG).show();
  1684. }
  1685. }
  1686. }
  1687. intent.removeExtra(ThreemaApplication.INTENT_DATA_CONTACT);
  1688. if (this.identity == null || this.identity.length() == 0 || this.identity.equals(this.userService.getIdentity())) {
  1689. logger.error("no identity found");
  1690. activity.finish();
  1691. return;
  1692. }
  1693. this.contactModel = this.contactService.getByIdentity(this.identity);
  1694. if (this.contactModel == null) {
  1695. Toast.makeText(getContext(), getString(R.string.contact_not_found) + ": " + this.identity, Toast.LENGTH_LONG).show();
  1696. Intent homeIntent = new Intent(activity, HomeActivity.class);
  1697. homeIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
  1698. startActivity(homeIntent);
  1699. activity.overridePendingTransition(0, 0);
  1700. activity.finish();
  1701. return;
  1702. }
  1703. this.messageReceiver = this.contactService.createReceiver(this.contactModel);
  1704. this.typingIndicatorTextWatcher = new TypingIndicatorTextWatcher(this.userService, contactModel);
  1705. this.conversationUid = ConversationUtil.getIdentityConversationUid(this.identity);
  1706. }
  1707. if (this.messageReceiver == null) {
  1708. logger.error("invalid receiver", activity, new Runnable() {
  1709. @Override
  1710. public void run() {
  1711. activity.finish();
  1712. }
  1713. });
  1714. return;
  1715. }
  1716. // hide chat from view and prevent screenshots - may not work on some devices
  1717. if (this.hiddenChatsListService.has(this.messageReceiver.getUniqueIdString())) {
  1718. try {
  1719. activity.getWindow().addFlags(FLAG_SECURE);
  1720. } catch (Exception ignored) { }
  1721. }
  1722. // set wallpaper based on message receiver
  1723. this.setBackgroundWallpaper();
  1724. // report shortcut as used
  1725. if (preferenceService.isDirectShare()) {
  1726. try {
  1727. ShortcutManagerCompat.reportShortcutUsed(activity, this.messageReceiver.getUniqueIdString());
  1728. } catch (IllegalStateException e) {
  1729. logger.debug("Failed to report shortcut use", e);
  1730. }
  1731. }
  1732. this.initConversationList(intent.hasExtra(EXTRA_API_MESSAGE_ID) && intent.hasExtra(EXTRA_SEARCH_QUERY) ? (Runnable) () -> {
  1733. String apiMessageId = intent.getStringExtra(EXTRA_API_MESSAGE_ID);
  1734. String searchQuery = intent.getStringExtra(EXTRA_SEARCH_QUERY);
  1735. AbstractMessageModel targetMessageModel = messageService.getMessageModelByApiMessageId(apiMessageId, messageReceiver.getType());
  1736. if (targetMessageModel != null && !TestUtil.empty(apiMessageId) && !TestUtil.empty(searchQuery)) {
  1737. String identity;
  1738. if (targetMessageModel instanceof GroupMessageModel) {
  1739. identity = targetMessageModel.isOutbox() ? contactService.getMe().getIdentity() : targetMessageModel.getIdentity();
  1740. } else {
  1741. identity = targetMessageModel.getIdentity();
  1742. }
  1743. QuoteUtil.QuoteContent quoteContent = QuoteUtil.QuoteContent.createV2(
  1744. identity,
  1745. searchQuery,
  1746. searchQuery,
  1747. apiMessageId,
  1748. targetMessageModel,
  1749. messageReceiver.getType(),
  1750. null,
  1751. null
  1752. );
  1753. if (composeMessageAdapter != null) {
  1754. ComposeMessageAdapter.ConversationListFilter filter = (ComposeMessageAdapter.ConversationListFilter) composeMessageAdapter.getQuoteFilter(quoteContent);
  1755. searchV2Quote(apiMessageId, filter);
  1756. intent.removeExtra(EXTRA_API_MESSAGE_ID);
  1757. }
  1758. } else {
  1759. Toast.makeText(ThreemaApplication.getAppContext(), R.string.message_not_found, Toast.LENGTH_SHORT).show();
  1760. }
  1761. } : null);
  1762. // work around the problem that the same original intent may be sent
  1763. // each time a singleTop activity (like this one) is coming back to front
  1764. // causing - in this case - duplicate delivery of forwarded messages.
  1765. // so we make sure the intent is only handled if it's newer than
  1766. // any previously handled intent
  1767. long newTimestamp = 0L;
  1768. try {
  1769. newTimestamp = intent.getLongExtra(ThreemaApplication.INTENT_DATA_TIMESTAMP, 0L);
  1770. if (newTimestamp != 0L && newTimestamp <= this.intentTimestamp) {
  1771. return;
  1772. }
  1773. } finally {
  1774. this.intentTimestamp = newTimestamp;
  1775. }
  1776. this.messageText.setText("");
  1777. this.messageText.setMessageReceiver(this.messageReceiver);
  1778. this.openBallotNoticeView.setMessageReceiver(this.messageReceiver);
  1779. this.openBallotNoticeView.setVisibilityListener(this);
  1780. // restore draft before setting predefined text
  1781. restoreMessageDraft();
  1782. String defaultText = intent.getStringExtra(ThreemaApplication.INTENT_DATA_TEXT);
  1783. if (!TestUtil.empty(defaultText)) {
  1784. this.messageText.setText(null);
  1785. this.messageText.append(defaultText);
  1786. }
  1787. updateSendButton(this.messageText.getText());
  1788. updateCameraButton();
  1789. boolean editFocus = intent.getBooleanExtra(ThreemaApplication.INTENT_DATA_EDITFOCUS, false);
  1790. if (editFocus || this.unreadCount <= 0) {
  1791. messageText.setSelected(true);
  1792. messageText.requestFocus();
  1793. }
  1794. this.notificationService.setVisibleReceiver(this.messageReceiver);
  1795. if (!this.isGroupChat && !this.isDistributionListChat) {
  1796. this.messageText.addTextChangedListener(this.typingIndicatorTextWatcher);
  1797. }
  1798. ListenerManager.chatListener.handle(listener -> listener.onChatOpened(conversationUid));
  1799. }
  1800. private boolean validateSendingPermission() {
  1801. return this.messageReceiver != null
  1802. && this.messageReceiver.validateSendingPermission(new MessageReceiver.OnSendingPermissionDenied() {
  1803. @Override
  1804. public void denied(final int errorResId) {
  1805. RuntimeUtil.runOnUiThread(() -> SingleToast.getInstance().showLongText(getString(errorResId)));
  1806. }
  1807. });
  1808. }
  1809. private void deleteSelectedMessages() {
  1810. int deleteableMessagesCount = 0;
  1811. if (selectedMessages != null && selectedMessages.size() > 0) {
  1812. // sort highest first for removal
  1813. Collections.sort(selectedMessages, new Comparator<AbstractMessageModel>() {
  1814. @Override
  1815. public int compare(AbstractMessageModel lhs, AbstractMessageModel rhs) {
  1816. return rhs.getId() - lhs.getId();
  1817. }
  1818. });
  1819. synchronized (deleteableMessages) {
  1820. for (AbstractMessageModel messageModel : selectedMessages) {
  1821. if (messageModel != null) {
  1822. // remove from adapter but not from database
  1823. int position = composeMessageAdapter.getPosition(messageModel);
  1824. AbstractMessageModel previousMessage = null;
  1825. if (position > 0) {
  1826. previousMessage = composeMessageAdapter.getItem(position - 1);
  1827. }
  1828. if (previousMessage != null && previousMessage instanceof DateSeparatorMessageModel) {
  1829. AbstractMessageModel nextMessage = null;
  1830. if (position < (composeMessageAdapter.getCount() - 1)) {
  1831. nextMessage = composeMessageAdapter.getItem(position + 1);
  1832. }
  1833. if (nextMessage == null ||
  1834. !dayFormatter.format(messageModel.getCreatedAt()).equals(dayFormatter.format(nextMessage.getCreatedAt()))) {
  1835. deleteableMessages.add(new Pair<>(previousMessage, position - 1));
  1836. composeMessageAdapter.remove(previousMessage);
  1837. }
  1838. }
  1839. deleteableMessages.add(new Pair<>(messageModel, position));
  1840. deleteableMessagesCount++;
  1841. composeMessageAdapter.remove(messageModel);
  1842. }
  1843. }
  1844. composeMessageAdapter.notifyDataSetChanged();
  1845. // sort lowest first for insertion
  1846. Collections.sort(deleteableMessages, new Comparator<Pair<AbstractMessageModel, Integer>>() {
  1847. @Override
  1848. public int compare(Pair<AbstractMessageModel, Integer> lhs, Pair<AbstractMessageModel, Integer> rhs) {
  1849. return lhs.second - rhs.second;
  1850. }
  1851. });
  1852. }
  1853. selectedMessages.clear();
  1854. if (actionMode != null) {
  1855. actionMode.finish();
  1856. }
  1857. try {
  1858. deleteSnackbar = Snackbar.make(coordinatorLayout, deleteableMessagesCount + " " + getString(R.string.message_deleted), 7 * (int) DateUtils.SECOND_IN_MILLIS);
  1859. deleteSnackbar.setAction(R.string.message_delete_undo, v -> RuntimeUtil.runOnUiThread(this::undoDeleteMessages));
  1860. deleteSnackbar.setCallback(new Snackbar.Callback() {
  1861. @Override
  1862. public void onDismissed(Snackbar snackbar, int event) {
  1863. super.onDismissed(snackbar, event);
  1864. if (event != DISMISS_EVENT_ACTION && event != DISMISS_EVENT_CONSECUTIVE && event != DISMISS_EVENT_MANUAL) {
  1865. RuntimeUtil.runOnUiThread(() -> deleteDeleteableMessages());
  1866. }
  1867. }
  1868. });
  1869. deleteSnackbar.show();
  1870. } catch (Exception e) {
  1871. logger.debug("https://issuetracker.google.com/issues/63793040");
  1872. RuntimeUtil.runOnUiThread(this::undoDeleteMessages);
  1873. }
  1874. } else {
  1875. if (actionMode != null) {
  1876. actionMode.finish();
  1877. }
  1878. }
  1879. }
  1880. private void undoDeleteMessages() {
  1881. synchronized (deleteableMessages) {
  1882. for (Pair<AbstractMessageModel, Integer> m : deleteableMessages) {
  1883. composeMessageAdapter.insert(m.first, m.second);
  1884. }
  1885. deleteableMessages.clear();
  1886. }
  1887. composeMessageAdapter.notifyDataSetChanged();
  1888. }
  1889. private synchronized void deleteDeleteableMessages() {
  1890. if (deleteableMessages.size() > 0) {
  1891. synchronized (deleteableMessages) {
  1892. for (Pair<AbstractMessageModel, Integer> m : deleteableMessages) {
  1893. if (m != null) {
  1894. messageService.remove(m.first);
  1895. }
  1896. }
  1897. deleteableMessages.clear();
  1898. if (messageReceiver != null) {
  1899. if (messageReceiver.getMessagesCount() <= 0 && messageReceiver instanceof ContactMessageReceiver) {
  1900. conversationService.clear(messageReceiver);
  1901. } else {
  1902. conversationService.refresh(messageReceiver);
  1903. }
  1904. }
  1905. }
  1906. }
  1907. }
  1908. @UiThread
  1909. private void contactTypingStateChanged(boolean isTyping) {
  1910. RuntimeUtil.runOnUiThread(new Runnable() {
  1911. @Override
  1912. public void run() {
  1913. if(isTypingView != null) {
  1914. logger.debug("is typing " + isTyping + " footer view count " + convListView.getFooterViewsCount());
  1915. if (isTyping) {
  1916. //remove if the the another footer element added
  1917. if(convListView.getFooterViewsCount() == 0) {
  1918. isTypingView.setVisibility(View.VISIBLE);
  1919. convListView.addFooterView(isTypingView, null, false);
  1920. }
  1921. } else {
  1922. removeIsTypingFooter();
  1923. }
  1924. }
  1925. }
  1926. });
  1927. }
  1928. private void removeIsTypingFooter() {
  1929. if (isTypingView != null) {
  1930. isTypingView.setVisibility(View.GONE);
  1931. if (convListView != null && convListView.getFooterViewsCount() > 0){
  1932. convListView.removeFooterView(isTypingView);
  1933. }
  1934. }
  1935. }
  1936. @UiThread
  1937. private boolean addMessageToList(AbstractMessageModel message) {
  1938. if (message == null || this.messageReceiver == null || this.composeMessageAdapter == null) {
  1939. return false;
  1940. }
  1941. //check if the message already added
  1942. if (this.listInitializedAt != null && message.getCreatedAt().before(this.listInitializedAt)) {
  1943. return false;
  1944. }
  1945. if (!this.messageReceiver.isMessageBelongsToMe(message)) {
  1946. //do nothing, not my thread
  1947. return false;
  1948. }
  1949. logger.debug("addMessageToList: started");
  1950. this.composeMessageAdapter.removeFirstUnreadPosition();
  1951. // if previous message is from another date, add a date separator
  1952. synchronized (this.messageValues) {
  1953. int size = this.messageValues.size();
  1954. Date date = new Date();
  1955. Date createdAt = size > 0 ? this.messageValues.get(size - 1).getCreatedAt() : new Date(0L);
  1956. if (!dayFormatter.format(createdAt).equals(dayFormatter.format(date))) {
  1957. final DateSeparatorMessageModel dateSeparatorMessageModel = new DateSeparatorMessageModel();
  1958. dateSeparatorMessageModel.setCreatedAt(date);
  1959. this.messageValues.add(size, dateSeparatorMessageModel);
  1960. }
  1961. }
  1962. this.composeMessageAdapter.add(message);
  1963. if (!this.isPaused) {
  1964. new Thread(
  1965. new ReadMessagesRoutine(Arrays.asList(message),
  1966. this.messageService,
  1967. this.notificationService)
  1968. ).start();
  1969. } else {
  1970. this.unreadMessages.add(message);
  1971. }
  1972. if (message.isOutbox()) {
  1973. // scroll to bottom on outgoing message
  1974. scrollList(Integer.MAX_VALUE);
  1975. }
  1976. logger.debug("addMessageToList: finished");
  1977. return true;
  1978. }
  1979. @UiThread
  1980. private void scrollList(final int targetPosition) {
  1981. logger.debug("scrollList " + targetPosition);
  1982. if (this.listUpdateInProgress) {
  1983. logger.debug("Update in progress");
  1984. return;
  1985. }
  1986. if (this.composeMessageAdapter == null) {
  1987. return;
  1988. }
  1989. this.composeMessageAdapter.notifyDataSetChanged();
  1990. this.convListView.post(new Runnable() {
  1991. @Override
  1992. public void run() {
  1993. int topEntry = convListView.getFirstVisiblePosition();
  1994. // update only if really necessary
  1995. if (targetPosition != topEntry) {
  1996. listUpdateInProgress = true;
  1997. int listEntryCount = convListView.getCount();
  1998. if (topEntry > targetPosition) {
  1999. // scroll up
  2000. int startPosition = targetPosition + SMOOTHSCROLL_THRESHOLD;
  2001. if (startPosition < listEntryCount) {
  2002. convListView.setSelection(targetPosition);
  2003. } else {
  2004. convListView.smoothScrollToPosition(targetPosition);
  2005. }
  2006. } else {
  2007. // scroll down
  2008. int startPosition = listEntryCount - SMOOTHSCROLL_THRESHOLD;
  2009. if (listEntryCount - convListView.getLastVisiblePosition() > SMOOTHSCROLL_THRESHOLD && startPosition > 0) {
  2010. convListView.setSelection(targetPosition);
  2011. } else {
  2012. convListView.smoothScrollToPosition(targetPosition);
  2013. }
  2014. }
  2015. listUpdateInProgress = false;
  2016. }
  2017. }
  2018. });
  2019. }
  2020. /**
  2021. * Loading the next records for the listview
  2022. */
  2023. @WorkerThread
  2024. private List<AbstractMessageModel> getNextRecords() {
  2025. List<AbstractMessageModel> messageModels = this.messageService.getMessagesForReceiver(this.messageReceiver, this.nextMessageFilter);
  2026. this.valuesLoaded(messageModels);
  2027. return messageModels;
  2028. }
  2029. @WorkerThread
  2030. private List<AbstractMessageModel> getAllRecords() {
  2031. List<AbstractMessageModel> messageModels = this.messageService.getMessagesForReceiver(this.messageReceiver);
  2032. this.valuesLoaded(messageModels);
  2033. return messageModels;
  2034. }
  2035. /**
  2036. * Append records to the list, adding date separators if necessary
  2037. * Locks list by calling setNotifyOnChange(false) on the adapter to speed up list ctrl
  2038. * Don't forget to call notifyDataSetChanged() on the adapter in the UI thread after inserting
  2039. * @param values MessageModels to insert
  2040. * @param clear Whether previous list entries should be cleared before appending
  2041. * @param markasread Whether chat should be marked as read
  2042. * @return Number of items that have been added to the list INCLUDING date separators and other decoration
  2043. */
  2044. @UiThread
  2045. private int insertToList(final List<AbstractMessageModel> values, boolean clear, boolean markasread, boolean notify) {
  2046. int insertedSize = 0;
  2047. this.composeMessageAdapter.setNotifyOnChange(false);
  2048. synchronized (this.messageValues) {
  2049. int initialSize = this.messageValues.size();
  2050. Date date = new Date();
  2051. if (clear) {
  2052. this.messageValues.clear();
  2053. } else {
  2054. // prevent duplicate date separators when adding messages to an existing chat (e.g. after pull-to-refresh)
  2055. if (this.messageValues.size() > 0) {
  2056. if (this.messageValues.get(0) instanceof DateSeparatorMessageModel) {
  2057. this.messageValues.remove(0);
  2058. }
  2059. AbstractMessageModel topmostMessage = this.messageValues.get(0);
  2060. if (topmostMessage != null) {
  2061. Date topmostDate = topmostMessage.getCreatedAt();
  2062. if (topmostDate != null) {
  2063. date = topmostDate;
  2064. }
  2065. }
  2066. }
  2067. }
  2068. for (AbstractMessageModel m : values) {
  2069. Date createdAt = m.getCreatedAt();
  2070. if (createdAt != null) {
  2071. if (!dayFormatter.format(createdAt).equals(dayFormatter.format(date))) {
  2072. if (!this.messageValues.isEmpty()) {
  2073. final DateSeparatorMessageModel dateSeparatorMessageModel = new DateSeparatorMessageModel();
  2074. dateSeparatorMessageModel.setCreatedAt(this.messageValues.get(0).getCreatedAt());
  2075. this.messageValues.add(0, dateSeparatorMessageModel);
  2076. }
  2077. date = createdAt;
  2078. }
  2079. }
  2080. this.messageValues.add(0, m);
  2081. }
  2082. if (!this.messageValues.isEmpty() && !(this.messageValues.get(0) instanceof DateSeparatorMessageModel)) {
  2083. // add topmost date separator
  2084. final DateSeparatorMessageModel dateSeparatorMessageModel = new DateSeparatorMessageModel();
  2085. dateSeparatorMessageModel.setCreatedAt(this.messageValues.get(0).getCreatedAt());
  2086. this.messageValues.add(0, dateSeparatorMessageModel);
  2087. }
  2088. this.listInitializedAt = new Date();
  2089. insertedSize = this.messageValues.size() - initialSize;
  2090. }
  2091. if (clear) {
  2092. composeMessageAdapter.setNotifyOnChange(true);
  2093. composeMessageAdapter.notifyDataSetInvalidated();
  2094. } else {
  2095. if (notify) {
  2096. composeMessageAdapter.notifyDataSetChanged();
  2097. } else {
  2098. composeMessageAdapter.setNotifyOnChange(true);
  2099. }
  2100. }
  2101. if (markasread) {
  2102. markAsRead();
  2103. }
  2104. return insertedSize;
  2105. }
  2106. private void valuesLoaded(List<AbstractMessageModel> values) {
  2107. if(values != null && values.size() > 0) {
  2108. this.currentPageReferenceId = values.get(values.size()-1).getId();
  2109. }
  2110. }
  2111. /**
  2112. * initialize conversation list and set the unread message count
  2113. * @return number of unread messages
  2114. */
  2115. @SuppressLint({"StaticFieldLeak", "WrongThread"})
  2116. @UiThread
  2117. private void initConversationList(@Nullable Runnable runAfter) {
  2118. this.unreadCount = (int) this.messageReceiver.getUnreadMessagesCount();
  2119. if (this.unreadCount > MESSAGE_PAGE_SIZE) {
  2120. new AsyncTask<Void, Void, List<AbstractMessageModel>>() {
  2121. @Override
  2122. protected void onPreExecute() {
  2123. GenericProgressDialog.newInstance(-1, R.string.please_wait).show(getParentFragmentManager(), DIALOG_TAG_LOADING_MESSAGES);
  2124. }
  2125. @Override
  2126. protected List<AbstractMessageModel> doInBackground(Void... voids) {
  2127. return messageService.getMessagesForReceiver(messageReceiver, new MessageService.MessageFilter() {
  2128. @Override
  2129. public long getPageSize() {
  2130. return -1;
  2131. }
  2132. @Override
  2133. public Integer getPageReferenceId() {
  2134. return null;
  2135. }
  2136. @Override
  2137. public boolean withStatusMessages() {
  2138. return false;
  2139. }
  2140. @Override
  2141. public boolean withUnsaved() {
  2142. return false;
  2143. }
  2144. @Override
  2145. public boolean onlyUnread() {
  2146. return false;
  2147. }
  2148. @Override
  2149. public boolean onlyDownloaded() {
  2150. return false;
  2151. }
  2152. @Override
  2153. public MessageType[] types() {
  2154. return new MessageType[0];
  2155. }
  2156. @Override
  2157. public int[] contentTypes() {
  2158. return null;
  2159. }
  2160. });
  2161. }
  2162. @Override
  2163. protected void onPostExecute(List<AbstractMessageModel> values) {
  2164. valuesLoaded(values);
  2165. populateList(values);
  2166. DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_LOADING_MESSAGES, true);
  2167. if (runAfter != null) {
  2168. runAfter.run();
  2169. }
  2170. }
  2171. }.execute();
  2172. } else {
  2173. populateList(getNextRecords());
  2174. if (runAfter != null) {
  2175. runAfter.run();
  2176. }
  2177. }
  2178. }
  2179. /**
  2180. * Populate ListView with provided message models
  2181. * @param values
  2182. */
  2183. @UiThread
  2184. private void populateList(List<AbstractMessageModel> values) {
  2185. if (composeMessageAdapter != null) {
  2186. // re-use existing adapter (for example on tablets)
  2187. composeMessageAdapter.clear();
  2188. composeMessageAdapter.setThumbnailWidth(ConfigUtils.getPreferredThumbnailWidth(getContext(), false));
  2189. composeMessageAdapter.setGroupId(groupId);
  2190. composeMessageAdapter.setMessageReceiver(messageReceiver);
  2191. composeMessageAdapter.setUnreadMessagesCount(unreadCount);
  2192. insertToList(values, true, true, true);
  2193. updateToolbarTitle();
  2194. } else {
  2195. thumbnailCache = new ThumbnailCache<Integer>(null);
  2196. composeMessageAdapter = new ComposeMessageAdapter(
  2197. activity,
  2198. messagePlayerService,
  2199. messageValues,
  2200. userService,
  2201. contactService,
  2202. fileService,
  2203. messageService,
  2204. ballotService,
  2205. preferenceService,
  2206. downloadService,
  2207. licenseService,
  2208. messageReceiver,
  2209. convListView,
  2210. thumbnailCache,
  2211. ConfigUtils.getPreferredThumbnailWidth(getContext(), false),
  2212. ComposeMessageFragment.this,
  2213. unreadCount
  2214. );
  2215. //adding footer before setting the list adapter (android < 4.4)
  2216. if (null != convListView && !isGroupChat && !isDistributionListChat) {
  2217. //create the istyping instance for later use
  2218. isTypingView = layoutInflater.inflate(R.layout.conversation_list_item_typing, null);
  2219. convListView.addFooterView(isTypingView, null, false);
  2220. }
  2221. composeMessageAdapter.setGroupId(groupId);
  2222. composeMessageAdapter.setOnClickListener(new ComposeMessageAdapter.OnClickListener() {
  2223. @Override
  2224. public void resend(AbstractMessageModel messageModel) {
  2225. if (messageModel.isOutbox() && messageModel.getState() == MessageState.SENDFAILED && messageReceiver.isMessageBelongsToMe(messageModel)) {
  2226. try {
  2227. messageService.resendMessage(messageModel, messageReceiver, null);
  2228. } catch (Exception e) {
  2229. RuntimeUtil.runOnUiThread(new Runnable() {
  2230. @Override
  2231. public void run() {
  2232. Toast.makeText(getContext(), R.string.original_file_no_longer_avilable, Toast.LENGTH_LONG).show();
  2233. }
  2234. });
  2235. }
  2236. }
  2237. }
  2238. @Override
  2239. public void click(View view, int position, AbstractMessageModel messageModel) {
  2240. if (searchActionMode == null) {
  2241. onListItemClick(view, position, messageModel);
  2242. }
  2243. }
  2244. @Override
  2245. public void longClick(View view, int position, AbstractMessageModel messageModel) {
  2246. if (searchActionMode == null) {
  2247. onListItemLongClick(view, position);
  2248. }
  2249. }
  2250. @Override
  2251. public boolean touch(View view, MotionEvent motionEvent, AbstractMessageModel messageModel) {
  2252. if (listViewSwipeListener != null && searchActionMode == null) {
  2253. return listViewSwipeListener.onTouch(view, motionEvent);
  2254. }
  2255. return false;
  2256. }
  2257. @Override
  2258. public void avatarClick(View view, int position, AbstractMessageModel messageModel) {
  2259. if (messageModel != null && messageModel.getIdentity() != null) {
  2260. ContactModel contactModel = contactService.getByIdentity(messageModel.getIdentity());
  2261. if (contactModel != null) {
  2262. Intent intent;
  2263. if (messageModel instanceof GroupMessageModel || messageModel instanceof DistributionListMessageModel) {
  2264. intent = new Intent(getActivity(), ComposeMessageActivity.class);
  2265. intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
  2266. intent.setData((Uri.parse("foobar://" + SystemClock.elapsedRealtime())));
  2267. IntentDataUtil.append(contactModel, intent);
  2268. getActivity().finish();
  2269. } else {
  2270. intent = new Intent(getActivity(), ContactDetailActivity.class);
  2271. intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT_READONLY, true);
  2272. IntentDataUtil.append(contactModel, intent);
  2273. }
  2274. AnimationUtil.startActivityForResult(getActivity(), view, intent, 0);
  2275. }
  2276. }
  2277. }
  2278. @SuppressLint("DefaultLocale")
  2279. @Override
  2280. public void onSearchResultsUpdate(int searchResultsIndex, int searchResultsSize) {
  2281. RuntimeUtil.runOnUiThread(() -> {
  2282. if (searchCounter != null) {
  2283. try {
  2284. searchCounter.setText(String.format("%d / %d", searchResultsIndex, searchResultsSize));
  2285. } catch (Exception e) {
  2286. //
  2287. }
  2288. }
  2289. });
  2290. }
  2291. @Override
  2292. public void onSearchInProgress(boolean inProgress) {
  2293. RuntimeUtil.runOnUiThread(() -> {
  2294. if (searchNextButton != null && searchPreviousButton != null) {
  2295. try {
  2296. searchPreviousButton.setVisibility(inProgress ? View.INVISIBLE : View.VISIBLE);
  2297. searchNextButton.setVisibility(inProgress ? View.INVISIBLE : View.VISIBLE);
  2298. searchProgress.setVisibility(inProgress ? View.VISIBLE : View.INVISIBLE);
  2299. } catch (Exception e) {
  2300. //
  2301. }
  2302. }
  2303. });
  2304. }
  2305. });
  2306. insertToList(values, false, !hiddenChatsListService.has(messageReceiver.getUniqueIdString()), false);
  2307. convListView.setAdapter(composeMessageAdapter);
  2308. convListView.setItemsCanFocus(false);
  2309. convListView.setVisibility(View.VISIBLE);
  2310. }
  2311. setIdentityColors();
  2312. removeIsTypingFooter();
  2313. }
  2314. /**
  2315. * Jump to first unread message keeping in account shift caused by date separators and other decorations
  2316. * Currently depends on various globals...
  2317. */
  2318. @UiThread
  2319. private void jumpToFirstUnreadMessage() {
  2320. if (unreadCount > 0) {
  2321. synchronized (this.messageValues) {
  2322. int position = Math.min(convListView.getCount() - unreadCount, this.messageValues.size() - 1);
  2323. while (position >= 0) {
  2324. if (this.messageValues.get(position) instanceof FirstUnreadMessageModel) {
  2325. break;
  2326. }
  2327. position--;
  2328. }
  2329. unreadCount = 0;
  2330. if (position > 0) {
  2331. final int finalPosition = position;
  2332. logger.debug("jump to initial position " + finalPosition);
  2333. convListView.setSelection(finalPosition);
  2334. convListView.postDelayed(() -> {
  2335. convListView.setSelection(finalPosition);
  2336. }, 750);
  2337. return;
  2338. }
  2339. }
  2340. }
  2341. convListView.setSelection(Integer.MAX_VALUE);
  2342. }
  2343. private void setIdentityColors() {
  2344. logger.debug("setIdentityColors");
  2345. if (this.isGroupChat) {
  2346. Map<String, Integer> colors = this.groupService.getGroupMemberColors(this.groupModel);
  2347. if (ConfigUtils.getAppTheme(activity) == ConfigUtils.THEME_DARK) {
  2348. Map<String, Integer> darkColors = new HashMap<>();
  2349. @ColorInt final int bubbleColorRecv = activity.getResources().getColor(R.color.dark_bubble_receive);
  2350. // lighten up some colors to ensure better visibility if dark theme is enabled
  2351. for (Map.Entry<String, Integer> entry : colors.entrySet()) {
  2352. @ColorInt int newColor;
  2353. try {
  2354. newColor = entry.getValue();
  2355. if (ColorUtils.calculateContrast(newColor, bubbleColorRecv) <= 1.7) {
  2356. float[] hsl = new float[3];
  2357. ColorUtils.colorToHSL(entry.getValue(), hsl);
  2358. if (hsl[2] < 0.7f) {
  2359. hsl[2] = 0.7f; // pull up luminance
  2360. }
  2361. if (hsl[1] > 0.6f) {
  2362. hsl[1] = 0.6f; // tone down saturation
  2363. }
  2364. newColor = ColorUtils.HSLToColor(hsl);
  2365. }
  2366. darkColors.put(entry.getKey(), newColor);
  2367. } catch (Exception e) {
  2368. //
  2369. }
  2370. }
  2371. this.identityColors = darkColors;
  2372. } else {
  2373. this.identityColors = colors;
  2374. }
  2375. } else {
  2376. if (this.identityColors != null) {
  2377. this.identityColors.clear();
  2378. }
  2379. }
  2380. this.composeMessageAdapter.setIdentityColors(this.identityColors);
  2381. }
  2382. private void onListItemClick(View view, int position, AbstractMessageModel messageModel) {
  2383. if (view == null) {
  2384. return;
  2385. }
  2386. if (actionMode != null) {
  2387. if (selectedMessages.contains(messageModel)) {
  2388. // remove from selection
  2389. selectedMessages.remove(messageModel);
  2390. convListView.setItemChecked(position, false);
  2391. } else {
  2392. if (convListView.getCheckedItemCount() < MAX_SELECTED_ITEMS) {
  2393. // add this to selection
  2394. selectedMessages.add(messageModel);
  2395. convListView.setItemChecked(position, true);
  2396. } else {
  2397. convListView.setItemChecked(position, false);
  2398. }
  2399. }
  2400. final int checked = convListView.getCheckedItemCount();
  2401. if (checked > 0) {
  2402. // invalidate menu to update display => onPrepareActionMode()
  2403. actionMode.invalidate();
  2404. } else {
  2405. actionMode.finish();
  2406. }
  2407. } else {
  2408. if (view.isSelected()) {
  2409. view.setSelected(false);
  2410. }
  2411. if (convListView.isItemChecked(position)) {
  2412. convListView.setItemChecked(position, false);
  2413. }
  2414. // check if item is a quote
  2415. if (QuoteUtil.isQuoteV1(messageModel.getBody())) {
  2416. QuoteUtil.QuoteContent quoteContent = QuoteUtil.getQuoteContent(
  2417. messageModel,
  2418. messageReceiver.getType(),
  2419. false,
  2420. thumbnailCache,
  2421. getContext(),
  2422. this.messageService,
  2423. this.userService,
  2424. this.fileService
  2425. );
  2426. if (quoteContent != null) {
  2427. if (searchActionMode != null) {
  2428. searchActionMode.finish();
  2429. }
  2430. ComposeMessageAdapter.ConversationListFilter filter = (ComposeMessageAdapter.ConversationListFilter) composeMessageAdapter.getQuoteFilter(quoteContent);
  2431. // search for quoted text
  2432. filter.filter(quoteContent.quotedText, count -> {
  2433. if (count == 0) {
  2434. SingleToast.getInstance().showShortText(getString(R.string.quote_not_found));
  2435. }
  2436. });
  2437. }
  2438. } else if (messageModel.getQuotedMessageId() != null) {
  2439. QuoteUtil.QuoteContent quoteContent = QuoteUtil.getQuoteContent(
  2440. messageModel,
  2441. messageReceiver.getType(),
  2442. false,
  2443. thumbnailCache,
  2444. getContext(),
  2445. this.messageService,
  2446. this.userService,
  2447. this.fileService
  2448. );
  2449. if (quoteContent != null) {
  2450. if (searchActionMode != null) {
  2451. searchActionMode.finish();
  2452. }
  2453. AbstractMessageModel quotedMessageModel = messageService.getMessageModelByApiMessageId(messageModel.getQuotedMessageId(), messageReceiver.getType());
  2454. if (quotedMessageModel != null) {
  2455. ComposeMessageAdapter.ConversationListFilter filter = (ComposeMessageAdapter.ConversationListFilter) composeMessageAdapter.getQuoteFilter(quoteContent);
  2456. searchV2Quote(quotedMessageModel.getApiMessageId(), filter);
  2457. } else {
  2458. Toast.makeText(getContext().getApplicationContext(), R.string.quoted_message_deleted, Toast.LENGTH_SHORT).show();
  2459. }
  2460. }
  2461. }
  2462. }
  2463. }
  2464. /**
  2465. * Recursively search for message with provided apiMessageId in chat and gradually load more records to Adapter until matching message is found by provided Filter
  2466. * TODO: we should provide a static version of this that does not rely on globals
  2467. * @param apiMessageId to search for
  2468. * @param filter Filter to use for this search
  2469. */
  2470. @UiThread
  2471. synchronized private void searchV2Quote(final String apiMessageId, final ComposeMessageAdapter.ConversationListFilter filter) {
  2472. filter.filter("#" + apiMessageId, new Filter.FilterListener() {
  2473. @SuppressLint("StaticFieldLeak")
  2474. @Override
  2475. public void onFilterComplete(int count) {
  2476. if (count == 0) {
  2477. new AsyncTask<Void, Void, Integer>() {
  2478. List<AbstractMessageModel> messageModels;
  2479. @Override
  2480. protected Integer doInBackground(Void... params) {
  2481. messageModels = getNextRecords();
  2482. if (messageModels != null) {
  2483. return messageModels.size();
  2484. }
  2485. return null;
  2486. }
  2487. @Override
  2488. protected void onPostExecute(Integer result) {
  2489. if (getContext() != null) {
  2490. if (result != null && result > 0) {
  2491. insertToList(messageModels, false, false, true);
  2492. if (getFragmentManager() != null) {
  2493. if (getFragmentManager().findFragmentByTag(DIALOG_TAG_SEARCHING) == null) {
  2494. GenericProgressDialog.newInstance(R.string.searching, R.string.please_wait).show(getFragmentManager(), DIALOG_TAG_SEARCHING);
  2495. }
  2496. searchV2Quote(apiMessageId, filter);
  2497. }
  2498. } else {
  2499. SingleToast.getInstance().showShortText(getString(R.string.quote_not_found));
  2500. swipeRefreshLayout.setEnabled(false);
  2501. }
  2502. }
  2503. }
  2504. }.execute();
  2505. } else {
  2506. DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_SEARCHING, true);
  2507. }
  2508. }
  2509. });
  2510. }
  2511. @UiThread
  2512. private void onListItemLongClick(@NonNull View view, final int position) {
  2513. int viewType = composeMessageAdapter.getItemViewType(position);
  2514. if (viewType == ComposeMessageAdapter.TYPE_FIRST_UNREAD || viewType == ComposeMessageAdapter.TYPE_DATE_SEPARATOR) {
  2515. return;
  2516. }
  2517. selectedMessages.clear();
  2518. selectedMessages.add(composeMessageAdapter.getItem(position));
  2519. if (actionMode != null) {
  2520. convListView.clearChoices();
  2521. convListView.setItemChecked(position, true);
  2522. actionMode.invalidate();
  2523. } else {
  2524. convListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
  2525. convListView.setItemChecked(position, true);
  2526. view.setSelected(true);
  2527. actionMode = activity.startSupportActionMode(new ComposeMessageAction(position));
  2528. }
  2529. view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
  2530. // fix linkify on longclick problem
  2531. // see: http://stackoverflow.com/questions/16047215/android-how-to-stop-linkify-on-long-press
  2532. longClickItem = position;
  2533. }
  2534. private boolean isMuted() {
  2535. if (messageReceiver != null && mutedChatsListService != null) {
  2536. String uniqueId = messageReceiver.getUniqueIdString();
  2537. return !TestUtil.empty(uniqueId) && mutedChatsListService.has(uniqueId);
  2538. }
  2539. return false;
  2540. }
  2541. private boolean isMentionsOnly() {
  2542. if (messageReceiver != null && mentionOnlyChatsListService != null) {
  2543. String uniqueId = messageReceiver.getUniqueIdString();
  2544. return !TestUtil.empty(uniqueId) && mentionOnlyChatsListService.has(uniqueId);
  2545. }
  2546. return false;
  2547. }
  2548. private boolean isSilent() {
  2549. if (messageReceiver != null && ringtoneService != null) {
  2550. String uniqueId = messageReceiver.getUniqueIdString();
  2551. return !TestUtil.empty(uniqueId) && ringtoneService.hasCustomRingtone(uniqueId) && ringtoneService.isSilent(uniqueId, isGroupChat);
  2552. }
  2553. return false;
  2554. }
  2555. private void playInAppSound(final int resId, final boolean isVibrate) {
  2556. if (this.isMuted() || this.isSilent()) {
  2557. //do not play
  2558. return;
  2559. }
  2560. RuntimeUtil.runOnUiThread(new Runnable() {
  2561. @Override
  2562. public void run() {
  2563. int ringerMode = audioManager.getRingerMode();
  2564. boolean isSilent = (ringerMode == AudioManager.RINGER_MODE_SILENT
  2565. || ringerMode == AudioManager.RINGER_MODE_VIBRATE);
  2566. if (preferenceService.isInAppSounds() && !isSilent) {
  2567. MediaPlayerStateWrapper mediaPlayer = new MediaPlayerStateWrapper();
  2568. mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
  2569. .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
  2570. .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
  2571. .build());
  2572. mediaPlayer.setVolume(0.3f, 0.3f);
  2573. mediaPlayer.setStateListener(new MediaPlayerStateWrapper.StateListener() {
  2574. @Override
  2575. public void onCompletion(MediaPlayer mp) {
  2576. if (mp.isPlaying()) {
  2577. mp.stop();
  2578. }
  2579. mp.reset();
  2580. mp.release();
  2581. }
  2582. @Override
  2583. public void onPrepared(MediaPlayer mp) {}
  2584. });
  2585. try (AssetFileDescriptor afd = ComposeMessageFragment.this.getResources().openRawResourceFd(resId)) {
  2586. mediaPlayer.setDataSource(afd);
  2587. mediaPlayer.prepare();
  2588. mediaPlayer.start();
  2589. } catch (Exception e) {
  2590. logger.debug("could not play in-app sound.");
  2591. mediaPlayer.release();
  2592. }
  2593. }
  2594. if (preferenceService.isInAppVibrate() && isVibrate) {
  2595. Vibrator vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
  2596. if (vibrator != null) {
  2597. switch (ringerMode) {
  2598. case AudioManager.RINGER_MODE_VIBRATE:
  2599. case AudioManager.RINGER_MODE_NORMAL:
  2600. vibrator.vibrate(VIBRATION_MSEC);
  2601. break;
  2602. default:
  2603. break;
  2604. }
  2605. }
  2606. }
  2607. }
  2608. });
  2609. }
  2610. private void playSentSound() {
  2611. playInAppSound(R.raw.sent_message, false);
  2612. }
  2613. private void playReceivedSound() {
  2614. playInAppSound(R.raw.received_message, true);
  2615. }
  2616. private void sendTextMessage() {
  2617. if (!this.validateSendingPermission()) {
  2618. return;
  2619. }
  2620. if (!TestUtil.empty(this.messageText.getText())) {
  2621. final CharSequence message;
  2622. if (isQuotePanelShown()) {
  2623. message = QuoteUtil.quote(
  2624. this.messageText.getText().toString(),
  2625. quoteInfo.quoteIdentity,
  2626. quoteInfo.quoteText,
  2627. quoteInfo.messageModel
  2628. );
  2629. closeQuoteMode();
  2630. } else {
  2631. message = this.messageText.getText();
  2632. }
  2633. if (!TestUtil.empty(message)) {
  2634. // block send button to avoid double posting
  2635. this.messageText.setText("");
  2636. if (typingIndicatorTextWatcher != null) {
  2637. messageText.removeTextChangedListener(typingIndicatorTextWatcher);
  2638. }
  2639. if (typingIndicatorTextWatcher != null) {
  2640. messageText.addTextChangedListener(typingIndicatorTextWatcher);
  2641. }
  2642. //send stopped typing message
  2643. if (typingIndicatorTextWatcher != null) {
  2644. typingIndicatorTextWatcher.stopTyping();
  2645. }
  2646. new Thread(new Runnable() {
  2647. @Override
  2648. public void run() {
  2649. TextMessageSendAction.getInstance()
  2650. .sendTextMessage(new MessageReceiver[]{messageReceiver}, message.toString(), new SendAction.ActionHandler() {
  2651. @Override
  2652. public void onError(final String errorMessage) {
  2653. RuntimeUtil.runOnUiThread(() -> {
  2654. Toast.makeText(getActivity(), errorMessage, Toast.LENGTH_LONG).show();
  2655. if (!TestUtil.empty(message)) {
  2656. messageText.setText(message);
  2657. messageText.setSelection(messageText.length());
  2658. }
  2659. });
  2660. }
  2661. @Override
  2662. public void onWarning(String warning, boolean continueAction) {
  2663. }
  2664. @Override
  2665. public void onProgress(final int progress, final int total) {
  2666. }
  2667. @Override
  2668. public void onCompleted() {
  2669. RuntimeUtil.runOnUiThread(new Runnable() {
  2670. @Override
  2671. public void run() {
  2672. scrollList(Integer.MAX_VALUE);
  2673. if (ConfigUtils.isTabletLayout()) {
  2674. // remove draft right now to make sure conversations pane is updated
  2675. ThreemaApplication.putMessageDraft(messageReceiver.getUniqueIdString(), "");
  2676. }
  2677. }
  2678. });
  2679. }
  2680. });
  2681. }
  2682. }).start();
  2683. }
  2684. } else {
  2685. if (ConfigUtils.requestAudioPermissions(getActivity(), this, PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE)) {
  2686. attachVoiceMessage();
  2687. }
  2688. }
  2689. }
  2690. private void attachVoiceMessage() {
  2691. closeQuoteMode();
  2692. // stop all message players
  2693. if (this.messagePlayerService != null) {
  2694. this.messagePlayerService.pauseAll(SOURCE_AUDIORECORDER);
  2695. }
  2696. Intent intent = new Intent(activity, VoiceRecorderActivity.class);
  2697. IntentDataUtil.addMessageReceiverToIntent(intent, messageReceiver);
  2698. activity.startActivityForResult(intent, ACTIVITY_ID_VOICE_RECORDER);
  2699. activity.overridePendingTransition(R.anim.slide_in_left_short, 0);
  2700. }
  2701. private void copySelectedMessagesToClipboard() {
  2702. AbstractMessageModel messageModel = selectedMessages.get(0);
  2703. if (messageModel == null) {
  2704. logger.error("no message model", activity);
  2705. return;
  2706. }
  2707. String body = "";
  2708. for (AbstractMessageModel message : selectedMessages) {
  2709. if (body.length() > 0) {
  2710. body += "\n";
  2711. }
  2712. body += message.getType() == MessageType.TEXT ?
  2713. QuoteUtil.getMessageBody(message, false) :
  2714. message.getCaption();
  2715. }
  2716. try {
  2717. ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
  2718. if (clipboard != null) {
  2719. ClipData clipData = ClipData.newPlainText(null, body);
  2720. if (clipData != null) {
  2721. clipboard.setPrimaryClip(clipData);
  2722. Snackbar.make(coordinatorLayout, R.string.message_copied, Snackbar.LENGTH_SHORT).show();
  2723. }
  2724. }
  2725. } catch (Exception e) {
  2726. // Some Android 4.3 devices raise an IllegalStateException when writing to the clipboard
  2727. // while there is an active clipboard listener
  2728. // see https://code.google.com/p/android/issues/detail?id=58043
  2729. logger.error("Exception", e);
  2730. }
  2731. }
  2732. @SuppressLint("StaticFieldLeak")
  2733. private void shareMessages() {
  2734. if (selectedMessages.size() > 1) {
  2735. new AsyncTask<Void, Void, Void>() {
  2736. @Override
  2737. protected void onPreExecute() {
  2738. GenericProgressDialog.newInstance(R.string.decoding_message, R.string.please_wait).show(getFragmentManager(), DIALOG_TAG_DECRYPTING_MESSAGES);
  2739. }
  2740. @Override
  2741. protected Void doInBackground(Void... voids) {
  2742. fileService.loadDecryptedMessageFiles(selectedMessages, new FileService.OnDecryptedFilesComplete() {
  2743. @Override
  2744. public void complete(ArrayList<Uri> uris) {
  2745. shareMediaMessages(uris);
  2746. }
  2747. @Override
  2748. public void error(String message) {
  2749. RuntimeUtil.runOnUiThread(() -> Toast.makeText(activity, message, Toast.LENGTH_LONG).show());
  2750. }
  2751. });
  2752. return null;
  2753. }
  2754. @Override
  2755. protected void onPostExecute(Void aVoid) {
  2756. DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_DECRYPTING_MESSAGES, true);
  2757. }
  2758. }.execute();
  2759. } else {
  2760. final AbstractMessageModel messageModel = selectedMessages.get(0);
  2761. if (messageModel != null) {
  2762. fileService.loadDecryptedMessageFile(messageModel, new FileService.OnDecryptedFileComplete() {
  2763. @Override
  2764. public void complete(File decryptedFile) {
  2765. if (decryptedFile != null) {
  2766. String filename = null;
  2767. if (messageModel.getType() == MessageType.FILE) {
  2768. filename = messageModel.getFileData().getFileName();
  2769. }
  2770. shareMediaMessages(Collections.singletonList(fileService.getShareFileUri(decryptedFile, filename)));
  2771. } else {
  2772. messageService.shareTextMessage(activity, messageModel);
  2773. }
  2774. }
  2775. @Override
  2776. public void error(final String message) {
  2777. RuntimeUtil.runOnUiThread(() -> Toast.makeText(activity, message, Toast.LENGTH_LONG).show());
  2778. }
  2779. });
  2780. }
  2781. }
  2782. }
  2783. private void shareMediaMessages(List<Uri> uris) {
  2784. if (selectedMessages.size() == 1) {
  2785. ExpandableTextEntryDialog alertDialog = ExpandableTextEntryDialog.newInstance(
  2786. getString(R.string.share_media),
  2787. R.string.add_caption_hint, selectedMessages.get(0).getCaption(),
  2788. R.string.label_continue, R.string.cancel, true);
  2789. alertDialog.setData(uris);
  2790. alertDialog.setTargetFragment(this, 0);
  2791. alertDialog.show(getParentFragmentManager(), null);
  2792. } else {
  2793. messageService.shareMediaMessages(activity,
  2794. new ArrayList<>(selectedMessages),
  2795. new ArrayList<>(uris), null);
  2796. }
  2797. }
  2798. @Override
  2799. public void onYes(String tag, Object data, String text) {
  2800. List<Uri> uris = (List<Uri>) data;
  2801. messageService.shareMediaMessages(activity,
  2802. new ArrayList<>(selectedMessages),
  2803. new ArrayList<>(uris), text);
  2804. }
  2805. @Override
  2806. public void onContactSelected(String identity, int length, int insertPosition) {
  2807. Editable editable = this.messageText.getText();
  2808. if (insertPosition >= 0 && insertPosition <= editable.length()) {
  2809. editable.delete(insertPosition, insertPosition + length);
  2810. this.messageText.addMention(identity);
  2811. }
  2812. }
  2813. private void startQuoteMode(AbstractMessageModel messageModel, Runnable onFinishRunnable) {
  2814. if (messageModel == null) {
  2815. messageModel = selectedMessages.get(0);
  2816. }
  2817. String body = QuoteUtil.getMessageBody(messageModel, true);
  2818. if (!TestUtil.empty(body) || ConfigUtils.canCreateV2Quotes()) {
  2819. sendButton.setEnabled(messageText != null && !TestUtil.empty(messageText.getText()));
  2820. quoteInfo.quoteIdentity = messageModel.isOutbox() ? userService.getIdentity() : messageModel.getIdentity();
  2821. quoteInfo.quoteIdentityView.setText(
  2822. NameUtil.getQuoteName(quoteInfo.quoteIdentity, this.contactService, this.userService)
  2823. );
  2824. int color = ConfigUtils.getAccentColor(activity);
  2825. if (!messageModel.isOutbox()) {
  2826. if (isGroupChat) {
  2827. if (identityColors != null && identityColors.containsKey(quoteInfo.quoteIdentity)) {
  2828. color = identityColors.get(quoteInfo.quoteIdentity);
  2829. }
  2830. } else {
  2831. if (contactModel != null) {
  2832. color = contactModel.getColor();
  2833. }
  2834. }
  2835. }
  2836. quoteInfo.quoteBar.setBackgroundColor(color);
  2837. quoteInfo.quoteTextView.setText(emojiMarkupUtil.addTextSpans(activity, body, quoteInfo.quoteTextView, false, false));
  2838. quoteInfo.quoteText = body;
  2839. quoteInfo.messageModel = messageModel;
  2840. quoteInfo.quoteThumbnail.setVisibility(View.GONE);
  2841. quoteInfo.quoteTypeImage.setVisibility(View.GONE);
  2842. if (ConfigUtils.canCreateV2Quotes()) {
  2843. try {
  2844. Bitmap thumbnail = fileService.getMessageThumbnailBitmap(messageModel, thumbnailCache);
  2845. if (thumbnail != null) {
  2846. quoteInfo.quoteThumbnail.setImageBitmap(thumbnail);
  2847. quoteInfo.quoteThumbnail.setVisibility(View.VISIBLE);
  2848. }
  2849. } catch (Exception ignore) {
  2850. }
  2851. MessageUtil.MessageViewElement messageViewElement = MessageUtil.getViewElement(getContext(), messageModel);
  2852. if (messageViewElement.icon != null) {
  2853. quoteInfo.quoteTypeImage.setImageResource(messageViewElement.icon);
  2854. quoteInfo.quoteTypeImage.setVisibility(View.VISIBLE);
  2855. }
  2856. }
  2857. AnimationUtil.expand(quoteInfo.quotePanel, onFinishRunnable);
  2858. }
  2859. }
  2860. private void closeQuoteMode() {
  2861. quoteInfo.quoteIdentityView.setText("");
  2862. quoteInfo.quoteTextView.setText("");
  2863. if (isQuotePanelShown()) {
  2864. AnimationUtil.collapse(quoteInfo.quotePanel, () -> updateSendButton(messageText.getText()));
  2865. }
  2866. }
  2867. private boolean isQuotePanelShown() {
  2868. return quoteInfo.quotePanel != null && quoteInfo.quotePanel.getVisibility() == View.VISIBLE;
  2869. }
  2870. private void startForwardMessage() {
  2871. if (selectedMessages.size() > 0) {
  2872. if (selectedMessages.size() == 1) {
  2873. final AbstractMessageModel messageModel = selectedMessages.get(0);
  2874. if (messageModel.getType() == MessageType.TEXT) {
  2875. // allow editing before sending if it's a single text message
  2876. String body = QuoteUtil.getMessageBody(messageModel, false);
  2877. Intent intent = new Intent(activity, RecipientListBaseActivity.class);
  2878. intent.setType("text/plain");
  2879. intent.setAction(Intent.ACTION_SEND);
  2880. intent.putExtra(Intent.EXTRA_TEXT, body);
  2881. intent.putExtra(ThreemaApplication.INTENT_DATA_IS_FORWARD, true);
  2882. activity.startActivity(intent);
  2883. return;
  2884. }
  2885. }
  2886. FileUtil.forwardMessages(activity, RecipientListBaseActivity.class, selectedMessages);
  2887. }
  2888. }
  2889. private void showMessageLog() {
  2890. AbstractMessageModel messageModel = selectedMessages.get(0);
  2891. if (messageModel == null) {
  2892. return;
  2893. }
  2894. MessageDetailDialog.newInstance(R.string.message_log_title, messageModel.getId(), messageModel.getClass().toString()).
  2895. show(getFragmentManager(), "messageLog");
  2896. }
  2897. private void updateToolbarTitle() {
  2898. if (!TestUtil.required(
  2899. this.actionBar,
  2900. this.actionBarSubtitleImageView,
  2901. this.actionBarSubtitleTextView,
  2902. this.actionBarTitleTextView,
  2903. this.emojiMarkupUtil,
  2904. this.messageReceiver) || !requiredInstances()) {
  2905. return;
  2906. }
  2907. this.actionBarSubtitleTextView.setVisibility(View.GONE);
  2908. this.actionBarSubtitleImageView.setVisibility(View.GONE);
  2909. this.actionBarTitleTextView.setText(this.messageReceiver.getDisplayName());
  2910. this.actionBarTitleTextView.setPaintFlags(this.actionBarTitleTextView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
  2911. if (this.isGroupChat) {
  2912. if (!groupService.isGroupMember(this.groupModel)) {
  2913. this.actionBarTitleTextView.setPaintFlags(this.actionBarTitleTextView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
  2914. }
  2915. actionBarSubtitleTextView.setText(groupService.getMembersString(groupModel));
  2916. actionBarSubtitleTextView.setVisibility(View.VISIBLE);
  2917. actionBarAvatarView.setImageBitmap(groupService.getAvatar(groupModel, false));
  2918. actionBarAvatarView.setBadgeVisible(false);
  2919. } else if (this.isDistributionListChat) {
  2920. actionBarSubtitleTextView.setText(this.distributionListService.getMembersString(this.distributionListModel));
  2921. actionBarSubtitleTextView.setVisibility(View.VISIBLE);
  2922. actionBarAvatarView.setImageBitmap(distributionListService.getAvatar(distributionListModel, false));
  2923. actionBarAvatarView.setBadgeVisible(false);
  2924. } else {
  2925. if (contactModel != null) {
  2926. this.actionBarSubtitleImageView.setContactModel(contactModel);
  2927. this.actionBarSubtitleImageView.setVisibility(View.VISIBLE);
  2928. this.actionBarAvatarView.setImageBitmap(contactService.getAvatar(contactModel, false, true));
  2929. this.actionBarAvatarView.setBadgeVisible(contactService.showBadge(contactModel));
  2930. }
  2931. }
  2932. this.actionBarTitleTextView.invalidate();
  2933. this.actionBarSubtitleTextView.invalidate();
  2934. this.actionBarSubtitleImageView.invalidate();
  2935. }
  2936. @Override
  2937. public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
  2938. inflater.inflate(R.menu.fragment_compose_message, menu);
  2939. this.setupToolbar();
  2940. super.onCreateOptionsMenu(menu, inflater);
  2941. ConfigUtils.addIconsToOverflowMenu(getContext(), menu);
  2942. }
  2943. @Override
  2944. public void onPrepareOptionsMenu(Menu menu) {
  2945. this.callItem = menu.findItem(R.id.menu_threema_call);
  2946. this.deleteDistributionListItem = menu.findItem(R.id.menu_delete_distribution_list);
  2947. this.shortCutItem = menu.findItem(R.id.menu_shortcut);
  2948. this.mutedMenuItem = menu.findItem(R.id.menu_muted);
  2949. this.blockMenuItem = menu.findItem(R.id.menu_block_contact);
  2950. this.showOpenBallotWindowMenuItem = menu.findItem(R.id.menu_ballot_window_show);
  2951. this.showBallotsMenuItem = menu.findItem(R.id.menu_ballot_show_all);
  2952. this.showOpenGroupRequestsMenuItem = menu.findItem(R.id.menu_group_requests_show);
  2953. this.showAllGroupRequestsMenuItem = menu.findItem(R.id.menu_group_request_show_all);
  2954. // initialize menus
  2955. updateMenus();
  2956. // initialize various toolbar items
  2957. this.updateToolbarTitle();
  2958. }
  2959. @SuppressLint("StaticFieldLeak")
  2960. private void updateMenus() {
  2961. logger.debug("updateMenus");
  2962. if (!TestUtil.required(
  2963. this.callItem,
  2964. this.deleteDistributionListItem,
  2965. this.shortCutItem,
  2966. this.mutedMenuItem,
  2967. this.blockMenuItem,
  2968. this.showOpenBallotWindowMenuItem,
  2969. this.showAllGroupRequestsMenuItem,
  2970. this.showOpenGroupRequestsMenuItem,
  2971. isAdded()
  2972. )) {
  2973. return;
  2974. }
  2975. this.deleteDistributionListItem.setVisible(this.isDistributionListChat);
  2976. this.shortCutItem.setVisible(ShortcutManagerCompat.isRequestPinShortcutSupported(getAppContext()));
  2977. this.mutedMenuItem.setVisible(!this.isDistributionListChat && !(isGroupChat && groupService.isNotesGroup(groupModel)));
  2978. updateMuteMenu();
  2979. if (contactModel != null) {
  2980. this.blockMenuItem.setVisible(true);
  2981. updateBlockMenu();
  2982. contactTypingStateChanged(contactService.isTyping(contactModel.getIdentity()));
  2983. } else {
  2984. this.blockMenuItem.setVisible(false);
  2985. }
  2986. new AsyncTask<Void, Void, Long>() {
  2987. @Override
  2988. protected Long doInBackground(Void... voids) {
  2989. return ballotService.countBallots(new BallotService.BallotFilter() {
  2990. @Override
  2991. public MessageReceiver getReceiver() { return messageReceiver; }
  2992. @Override
  2993. public BallotModel.State[] getStates() { return new BallotModel.State[]{BallotModel.State.OPEN}; }
  2994. @Override
  2995. public String createdOrNotVotedByIdentity() {
  2996. return userService.getIdentity();
  2997. }
  2998. @Override
  2999. public boolean filter(BallotModel ballotModel) { return true; }
  3000. });
  3001. }
  3002. @Override
  3003. protected void onPostExecute(Long openBallots) {
  3004. showOpenBallotWindowMenuItem.setVisible(openBallots > 0L);
  3005. if (preferenceService.getBallotOverviewHidden()) {
  3006. showOpenBallotWindowMenuItem.setIcon(R.drawable.ic_outline_visibility);
  3007. showOpenBallotWindowMenuItem.setTitle(R.string.ballot_window_show);
  3008. } else {
  3009. showOpenBallotWindowMenuItem.setIcon(R.drawable.ic_outline_visibility_off);
  3010. showOpenBallotWindowMenuItem.setTitle(R.string.ballot_window_hide);
  3011. }
  3012. Context context = getContext();
  3013. if (context != null) {
  3014. ConfigUtils.themeMenuItem(showOpenBallotWindowMenuItem, ConfigUtils.getColorFromAttribute(context, R.attr.textColorSecondary));
  3015. }
  3016. }
  3017. }.execute();
  3018. new AsyncTask<Void, Void, Long>() {
  3019. @Override
  3020. protected Long doInBackground(Void... voids) {
  3021. return ballotService.countBallots(new BallotService.BallotFilter() {
  3022. @Override
  3023. public MessageReceiver getReceiver() { return messageReceiver; }
  3024. @Override
  3025. public BallotModel.State[] getStates() { return new BallotModel.State[]{BallotModel.State.OPEN, BallotModel.State.CLOSED}; }
  3026. @Override
  3027. public boolean filter(BallotModel ballotModel) { return true; }
  3028. });
  3029. }
  3030. @Override
  3031. protected void onPostExecute(Long hasBallots) {
  3032. showBallotsMenuItem.setVisible(hasBallots > 0L);
  3033. }
  3034. }.execute();
  3035. // show/hide open group request chips if there are any
  3036. if (ConfigUtils.supportsGroupLinks() && !databaseServiceNew.getIncomingGroupJoinRequestModelFactory()
  3037. .getDestictOpenRequestsForGroup(groupId).isEmpty()) {
  3038. showOpenGroupRequestsMenuItem.setVisible(true);
  3039. showAllGroupRequestsMenuItem.setVisible(true);
  3040. if (preferenceService.getGroupRequestsOverviewHidden()) {
  3041. showOpenGroupRequestsMenuItem.setIcon(R.drawable.ic_outline_visibility);
  3042. showOpenGroupRequestsMenuItem.setTitle(R.string.open_group_requests_show);
  3043. } else {
  3044. showOpenGroupRequestsMenuItem.setIcon(R.drawable.ic_outline_visibility_off);
  3045. showOpenGroupRequestsMenuItem.setTitle(R.string.open_group_requests_hide);
  3046. }
  3047. }
  3048. else {
  3049. showOpenGroupRequestsMenuItem.setVisible(false);
  3050. }
  3051. // link to incoming group requests overview if there are any (includes already answered ones)
  3052. showAllGroupRequestsMenuItem.setVisible(ConfigUtils.supportsGroupLinks() &&
  3053. !databaseServiceNew.getIncomingGroupJoinRequestModelFactory()
  3054. .getAllRequestsForGroup(groupId).isEmpty()
  3055. );
  3056. updateVoipCallMenuItem(null);
  3057. }
  3058. private void updateMuteMenu() {
  3059. if (!isAdded() || this.mutedMenuItem == null) {
  3060. // do not update if no longer attached to activity
  3061. return;
  3062. }
  3063. if (isMuted()) {
  3064. this.mutedMenuItem.setIcon(R.drawable.ic_dnd_total_silence_grey600_24dp);
  3065. this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
  3066. } else if (isMentionsOnly()) {
  3067. this.mutedMenuItem.setIcon(R.drawable.ic_dnd_mention_grey600_24dp);
  3068. this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
  3069. } else if (isSilent()) {
  3070. this.mutedMenuItem.setIcon(R.drawable.ic_notifications_off_outline);
  3071. this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
  3072. } else {
  3073. this.mutedMenuItem.setIcon(R.drawable.ic_notifications_active_outline);
  3074. this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
  3075. }
  3076. }
  3077. private void updateBlockMenu() {
  3078. if (!isAdded()) {
  3079. // do not update if no longer attached to activity
  3080. return;
  3081. }
  3082. if (TestUtil.required(this.blockMenuItem, this.blackListIdentityService, this.contactModel)) {
  3083. boolean state = this.blackListIdentityService.has(this.contactModel.getIdentity());
  3084. this.blockMenuItem.setTitle(state ? getString(R.string.unblock_contact) : getString(R.string.block_contact));
  3085. this.blockMenuItem.setShowAsAction(state ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER);
  3086. this.mutedMenuItem.setShowAsAction(state ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
  3087. this.mutedMenuItem.setVisible(!state);
  3088. this.callItem.setShowAsAction(state ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_ALWAYS);
  3089. updateVoipCallMenuItem(!state);
  3090. }
  3091. }
  3092. @AnyThread
  3093. private void updateVoipCallMenuItem(final Boolean newState) {
  3094. RuntimeUtil.runOnUiThread(new Runnable() {
  3095. @Override
  3096. public void run() {
  3097. if (callItem != null) {
  3098. if (ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService)
  3099. && ConfigUtils.isCallsEnabled(getContext(), preferenceService, licenseService)) {
  3100. logger.debug("updateVoipMenu newState " + newState);
  3101. callItem.setVisible(newState != null ? newState : voipStateService.getCallState().isIdle());
  3102. } else {
  3103. callItem.setVisible(false);
  3104. }
  3105. }
  3106. }
  3107. });
  3108. }
  3109. private Intent addExtrasToIntent(Intent intent, MessageReceiver receiver) {
  3110. switch (receiver.getType()) {
  3111. case MessageReceiver.Type_GROUP:
  3112. intent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, groupId);
  3113. break;
  3114. case MessageReceiver.Type_DISTRIBUTION_LIST:
  3115. intent.putExtra(ThreemaApplication.INTENT_DATA_DISTRIBUTION_LIST, distributionListModel.getId());
  3116. break;
  3117. case MessageReceiver.Type_CONTACT:
  3118. default:
  3119. intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, identity);
  3120. }
  3121. return intent;
  3122. }
  3123. private void attachCamera() {
  3124. Intent previewIntent = IntentDataUtil.addMessageReceiversToIntent(new Intent(activity, SendMediaActivity.class), new MessageReceiver[]{this.messageReceiver});
  3125. if (this.actionBarTitleTextView != null && this.actionBarTitleTextView.getText() != null) {
  3126. previewIntent.putExtra(ThreemaApplication.INTENT_DATA_TEXT, this.actionBarTitleTextView.getText().toString());
  3127. }
  3128. previewIntent.putExtra(ThreemaApplication.INTENT_DATA_PICK_FROM_CAMERA, true);
  3129. AnimationUtil.startActivityForResult(activity, null, previewIntent, ThreemaActivity.ACTIVITY_ID_SEND_MEDIA);
  3130. }
  3131. private void showPermissionRationale(int stringResource) {
  3132. ConfigUtils.showPermissionRationale(getContext(), coordinatorLayout, stringResource);
  3133. }
  3134. @Override
  3135. public boolean onOptionsItemSelected(MenuItem item) {
  3136. switch (item.getItemId()) {
  3137. case android.R.id.home:
  3138. NavigationUtil.navigateUpToHome(activity);
  3139. break;
  3140. case R.id.menu_search_messages:
  3141. searchActionMode = activity.startSupportActionMode(new SearchActionMode());
  3142. break;
  3143. case R.id.menu_gallery:
  3144. Intent mediaGalleryIntent = new Intent(activity, MediaGalleryActivity.class);
  3145. activity.startActivity(addExtrasToIntent(mediaGalleryIntent, this.messageReceiver));
  3146. break;
  3147. case R.id.menu_threema_call:
  3148. VoipUtil.initiateCall(activity, contactModel, false, null);
  3149. break;
  3150. case R.id.menu_wallpaper:
  3151. wallpaperService.selectWallpaper(this, this.messageReceiver, new Runnable() {
  3152. @Override
  3153. public void run() {
  3154. RuntimeUtil.runOnUiThread(new Runnable() {
  3155. @Override
  3156. public void run() {
  3157. setBackgroundWallpaper();
  3158. }
  3159. });
  3160. }
  3161. });
  3162. activity.overridePendingTransition(R.anim.fast_fade_in, R.anim.fast_fade_out);
  3163. break;
  3164. case R.id.menu_muted:
  3165. if (!isDistributionListChat) {
  3166. Intent intent;
  3167. int[] location = new int[2];
  3168. if (isGroupChat) {
  3169. intent = new Intent(activity, GroupNotificationsActivity.class);
  3170. intent.putExtra(ThreemaApplication.INTENT_DATA_GROUP, this.groupId);
  3171. } else {
  3172. intent = new Intent(activity, ContactNotificationsActivity.class);
  3173. intent.putExtra(ThreemaApplication.INTENT_DATA_CONTACT, this.identity);
  3174. }
  3175. if (ToolbarUtil.getMenuItemCenterPosition(((ThreemaToolbarActivity)activity).getToolbar(), R.id.menu_muted, location)) {
  3176. intent.putExtra((ThreemaApplication.INTENT_DATA_ANIM_CENTER), location);
  3177. }
  3178. activity.startActivity(intent);
  3179. }
  3180. break;
  3181. case R.id.menu_block_contact:
  3182. if (this.blackListIdentityService.has(contactModel.getIdentity())) {
  3183. this.blackListIdentityService.toggle(activity, contactModel);
  3184. updateBlockMenu();
  3185. } else {
  3186. GenericAlertDialog.newInstance(R.string.block_contact, R.string.really_block_contact, R.string.yes, R.string.no).setTargetFragment(this).show(getFragmentManager(), DIALOG_TAG_CONFIRM_BLOCK);
  3187. }
  3188. break;
  3189. case R.id.menu_delete_distribution_list:
  3190. GenericAlertDialog.newInstance(R.string.really_delete_distribution_list,
  3191. R.string.really_delete_distribution_list_message,
  3192. R.string.ok,
  3193. R.string.cancel)
  3194. .setTargetFragment(this)
  3195. .setData(distributionListModel)
  3196. .show(getFragmentManager(), CONFIRM_TAG_DELETE_DISTRIBUTION_LIST);
  3197. break;
  3198. case R.id.menu_shortcut:
  3199. createShortcut();
  3200. break;
  3201. case R.id.menu_empty_chat:
  3202. GenericAlertDialog.newInstance(R.string.empty_chat_title,
  3203. R.string.empty_chat_confirm,
  3204. R.string.ok,
  3205. R.string.cancel)
  3206. .setTargetFragment(this)
  3207. .show(getFragmentManager(), DIALOG_TAG_EMPTY_CHAT);
  3208. break;
  3209. case R.id.menu_ballot_window_show:
  3210. if (openBallotNoticeView.isShown()) {
  3211. preferenceService.setBallotOverviewHidden(true);
  3212. openBallotNoticeView.hide(true);
  3213. } else {
  3214. preferenceService.setBallotOverviewHidden(false);
  3215. openBallotNoticeView.show(true);
  3216. }
  3217. break;
  3218. case R.id.menu_ballot_show_all:
  3219. Intent intent = new Intent(getContext(), BallotOverviewActivity.class);
  3220. IntentDataUtil.addMessageReceiverToIntent(intent, messageReceiver);
  3221. startActivity(intent);
  3222. break;
  3223. case R.id.menu_group_request_show_all:
  3224. Intent groupRequestsOverviewIntent = new Intent(getContext(), IncomingGroupRequestActivity.class);
  3225. IntentDataUtil.addMessageReceiverToIntent(groupRequestsOverviewIntent, messageReceiver);
  3226. startActivity(groupRequestsOverviewIntent);
  3227. break;
  3228. case R.id.menu_group_requests_show:
  3229. if (openGroupRequestNoticeView.isShown()) {
  3230. preferenceService.setGroupRequestsOverviewHidden(true);
  3231. openGroupRequestNoticeView.hide(true);
  3232. } else {
  3233. preferenceService.setGroupRequestsOverviewHidden(false);
  3234. openGroupRequestNoticeView.show(true);
  3235. }
  3236. break;
  3237. }
  3238. return false;
  3239. }
  3240. private void emptyChat() {
  3241. new EmptyChatAsyncTask(messageReceiver, messageService, conversationService, getFragmentManager(), false, new Runnable() {
  3242. @Override
  3243. public void run() {
  3244. if (isAdded()) {
  3245. synchronized (messageValues) {
  3246. messageValues.clear();
  3247. composeMessageAdapter.notifyDataSetChanged();
  3248. }
  3249. // empty draft
  3250. ThreemaApplication.putMessageDraft(messageReceiver.getUniqueIdString(), "");
  3251. messageText.setText(null);
  3252. currentPageReferenceId = 0;
  3253. onRefresh();
  3254. ListenerManager.conversationListeners.handle(new ListenerManager.HandleListener<ConversationListener>() {
  3255. @Override
  3256. public void handle(ConversationListener listener) {
  3257. if (!isGroupChat) {
  3258. conversationService.reset();
  3259. }
  3260. listener.onModifiedAll();
  3261. }
  3262. });
  3263. if (getActivity() != null) {
  3264. getActivity().finish();
  3265. }
  3266. }
  3267. }
  3268. }).execute();
  3269. }
  3270. private void createShortcut() {
  3271. if (!this.isGroupChat &&
  3272. !this.isDistributionListChat &&
  3273. ContactUtil.canReceiveVoipMessages(contactModel, blackListIdentityService) &&
  3274. ConfigUtils.isCallsEnabled(getContext(), preferenceService, licenseService)) {
  3275. ArrayList<SelectorDialogItem> items = new ArrayList<>();
  3276. items.add(new SelectorDialogItem(getString(R.string.prefs_header_chat), R.drawable.ic_outline_chat_bubble_outline));
  3277. items.add(new SelectorDialogItem(getString(R.string.threema_call), R.drawable.ic_call_outline));
  3278. SelectorDialog selectorDialog = SelectorDialog.newInstance(getString(R.string.shortcut_choice_title), items, getString(R.string.cancel));
  3279. selectorDialog.setTargetFragment(this, 0);
  3280. selectorDialog.show(getFragmentManager(), DIALOG_TAG_CHOOSE_SHORTCUT_TYPE);
  3281. } else {
  3282. ShortcutUtil.createPinnedShortcut(messageReceiver, TYPE_CHAT);
  3283. }
  3284. }
  3285. private void sendMessage() {
  3286. if (typingIndicatorTextWatcher != null) {
  3287. typingIndicatorTextWatcher.killEvents();
  3288. }
  3289. this.sendTextMessage();
  3290. }
  3291. @Override
  3292. public void onActivityResult(int requestCode, int resultCode,
  3293. final Intent intent) {
  3294. if (wallpaperService != null && wallpaperService.handleActivityResult(this, requestCode, resultCode, intent, this.messageReceiver)) {
  3295. setBackgroundWallpaper();
  3296. return;
  3297. }
  3298. if (requestCode == ACTIVITY_ID_VOICE_RECORDER) {
  3299. if (this.messagePlayerService != null) {
  3300. this.messagePlayerService.resumeAll(getActivity(), messageReceiver, SOURCE_AUDIORECORDER);
  3301. }
  3302. }
  3303. if (requestCode == ThreemaActivity.ACTIVITY_ID_ATTACH_MEDIA && resultCode == Activity.RESULT_OK) {
  3304. this.lastMediaFilter = IntentDataUtil.getLastMediaFilterFromIntent(intent);
  3305. }
  3306. }
  3307. private final SearchView.OnQueryTextListener queryTextListener = new SearchView.OnQueryTextListener() {
  3308. // listener for search bar on top
  3309. @Override
  3310. public boolean onQueryTextChange(String newText) {
  3311. composeMessageAdapter.getFilter().filter(newText);
  3312. return true;
  3313. }
  3314. @Override
  3315. public boolean onQueryTextSubmit(String query) {
  3316. composeMessageAdapter.nextMatchPosition();
  3317. return true;
  3318. }
  3319. };
  3320. @Override
  3321. public void onClick(String tag, int which, Object data) {
  3322. if (DIALOG_TAG_CHOOSE_SHORTCUT_TYPE.equals(tag)) {
  3323. int shortcutType = which + 1;
  3324. ShortcutUtil.createPinnedShortcut(messageReceiver, shortcutType);
  3325. }
  3326. }
  3327. @Override
  3328. public void onCancel(String tag) {}
  3329. @Override
  3330. public void onNo(String tag) {}
  3331. public class ComposeMessageAction implements ActionMode.Callback {
  3332. private final int position;
  3333. private MenuItem ackItem, decItem, quoteItem, logItem, discardItem, forwardItem, saveItem, copyItem, qrItem, shareItem, showText;
  3334. ComposeMessageAction(int position) {
  3335. this.position = position;
  3336. longClickItem = position;
  3337. }
  3338. private void updateActionMenu() {
  3339. // workaround for support library bug, see https://code.google.com/p/android/issues/detail?id=81192
  3340. MenuItemCompat.setShowAsAction(ackItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
  3341. MenuItemCompat.setShowAsAction(decItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
  3342. MenuItemCompat.setShowAsAction(quoteItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
  3343. MenuItemCompat.setShowAsAction(logItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
  3344. MenuItemCompat.setShowAsAction(discardItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
  3345. MenuItemCompat.setShowAsAction(saveItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
  3346. MenuItemCompat.setShowAsAction(copyItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
  3347. MenuItemCompat.setShowAsAction(forwardItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
  3348. MenuItemCompat.setShowAsAction(qrItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
  3349. MenuItemCompat.setShowAsAction(shareItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
  3350. decItem.setVisible(false);
  3351. ackItem.setVisible(false);
  3352. quoteItem.setVisible(false);
  3353. qrItem.setVisible(false);
  3354. copyItem.setVisible(false);
  3355. logItem.setVisible(false);
  3356. saveItem.setVisible(false);
  3357. shareItem.setVisible(false);
  3358. showText.setVisible(false);
  3359. if (selectedMessages.size() > 1) {
  3360. boolean isForwardable = selectedMessages.size() <= MAX_FORWARDABLE_ITEMS;
  3361. boolean isMedia = true;
  3362. boolean isTextOnly = true;
  3363. boolean isShareable = true;
  3364. for (AbstractMessageModel message: selectedMessages) {
  3365. if (isForwardable
  3366. && (
  3367. // if the media is not downloaded
  3368. !message.isAvailable()
  3369. // or the message is status message (unread or status)
  3370. || message.isStatusMessage()
  3371. // or a ballot
  3372. || message.getType() == MessageType.BALLOT
  3373. // or a voip status
  3374. || message.getType() == MessageType.VOIP_STATUS)) {
  3375. isForwardable = false;
  3376. }
  3377. if (isMedia && !message.isAvailable() ||
  3378. (message.getType() != MessageType.IMAGE &&
  3379. message.getType() != MessageType.VOICEMESSAGE &&
  3380. message.getType() != MessageType.VIDEO &&
  3381. message.getType() != MessageType.FILE)) {
  3382. isMedia = false;
  3383. }
  3384. if (isTextOnly && message.getType() != MessageType.TEXT) {
  3385. isTextOnly = false;
  3386. }
  3387. if (isShareable) {
  3388. if (message.getType() != MessageType.IMAGE && message.getType() != MessageType.VIDEO && message.getType() != MessageType.FILE) {
  3389. isShareable = false;
  3390. }
  3391. }
  3392. }
  3393. forwardItem.setVisible(isForwardable);
  3394. saveItem.setVisible(isMedia);
  3395. copyItem.setVisible(isTextOnly);
  3396. shareItem.setVisible(isShareable);
  3397. } else if (selectedMessages.size() == 1) {
  3398. AbstractMessageModel selectedMessage = selectedMessages.get(0);
  3399. if (selectedMessage.isStatusMessage()) {
  3400. forwardItem.setVisible(false);
  3401. copyItem.setVisible(true);
  3402. logItem.setVisible(true);
  3403. } else {
  3404. boolean isValidReceiver = messageReceiver.validateSendingPermission(null);
  3405. quoteItem.setVisible(isValidReceiver && QuoteUtil.isQuoteable(selectedMessage));
  3406. decItem.setVisible(MessageUtil.canSendUserDecline(selectedMessage) && isValidReceiver);
  3407. ackItem.setVisible(MessageUtil.canSendUserAcknowledge(selectedMessage) && isValidReceiver);
  3408. logItem.setVisible(true);
  3409. switch (selectedMessage.getType()) {
  3410. case IMAGE:
  3411. saveItem.setVisible(true);
  3412. forwardItem.setVisible(true);
  3413. shareItem.setVisible(true);
  3414. if (!TestUtil.empty(selectedMessage.getCaption())) {
  3415. copyItem.setVisible(true);
  3416. }
  3417. break;
  3418. case VIDEO:
  3419. saveItem.setVisible(selectedMessage.isAvailable());
  3420. forwardItem.setVisible(selectedMessage.isAvailable());
  3421. shareItem.setVisible(selectedMessage.isAvailable());
  3422. break;
  3423. case VOICEMESSAGE:
  3424. saveItem.setVisible(selectedMessage.isAvailable());
  3425. forwardItem.setVisible(selectedMessage.isAvailable());
  3426. break;
  3427. case FILE:
  3428. if (selectedMessage.getFileData().getRenderingType() == FileData.RENDERING_DEFAULT) {
  3429. MenuItemCompat.setShowAsAction(saveItem, MenuItemCompat.SHOW_AS_ACTION_ALWAYS);
  3430. MenuItemCompat.setShowAsAction(forwardItem, MenuItemCompat.SHOW_AS_ACTION_IF_ROOM);
  3431. }
  3432. saveItem.setVisible(selectedMessage.isAvailable());
  3433. shareItem.setVisible(selectedMessage.isAvailable());
  3434. forwardItem.setVisible(selectedMessage.isAvailable());
  3435. if (!TestUtil.empty(selectedMessage.getCaption())) {
  3436. copyItem.setVisible(true);
  3437. }
  3438. break;
  3439. case BALLOT:
  3440. saveItem.setVisible(false);
  3441. forwardItem.setVisible(false);
  3442. break;
  3443. case TEXT:
  3444. saveItem.setVisible(false);
  3445. forwardItem.setVisible(true);
  3446. copyItem.setVisible(true);
  3447. qrItem.setVisible(true);
  3448. shareItem.setVisible(true);
  3449. showText.setVisible(true);
  3450. break;
  3451. case VOIP_STATUS:
  3452. saveItem.setVisible(false);
  3453. forwardItem.setVisible(false);
  3454. copyItem.setVisible(false);
  3455. qrItem.setVisible(false);
  3456. shareItem.setVisible(false);
  3457. logItem.setVisible(false);
  3458. break;
  3459. case LOCATION:
  3460. shareItem.setVisible(true);
  3461. break;
  3462. default:
  3463. break;
  3464. }
  3465. }
  3466. }
  3467. if (AppRestrictionUtil.isShareMediaDisabled(getContext())) {
  3468. shareItem.setVisible(false);
  3469. saveItem.setVisible(false);
  3470. }
  3471. }
  3472. @Override
  3473. public boolean onCreateActionMode(ActionMode mode, Menu menu) {
  3474. if (this.position == AbsListView.INVALID_POSITION) {
  3475. return false;
  3476. }
  3477. if (convListView.getCheckedItemCount() < 1) {
  3478. return false;
  3479. }
  3480. MenuInflater inflater = mode.getMenuInflater();
  3481. if (inflater != null) {
  3482. inflater.inflate(R.menu.action_compose_message, menu);
  3483. }
  3484. ConfigUtils.addIconsToOverflowMenu(null, menu);
  3485. decItem = menu.findItem(R.id.menu_message_dec);
  3486. ackItem = menu.findItem(R.id.menu_message_ack);
  3487. logItem = menu.findItem(R.id.menu_message_log);
  3488. discardItem = menu.findItem(R.id.menu_message_discard);
  3489. forwardItem = menu.findItem(R.id.menu_message_forward);
  3490. saveItem = menu.findItem(R.id.menu_message_save);
  3491. copyItem = menu.findItem(R.id.menu_message_copy);
  3492. qrItem = menu.findItem(R.id.menu_message_qrcode);
  3493. shareItem = menu.findItem(R.id.menu_share);
  3494. quoteItem = menu.findItem(R.id.menu_message_quote);
  3495. showText = menu.findItem(R.id.menu_show_text);
  3496. updateActionMenu();
  3497. return true;
  3498. }
  3499. @Override
  3500. public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
  3501. final int checked = convListView.getCheckedItemCount();
  3502. mode.setTitle(Integer.toString(checked));
  3503. updateActionMenu();
  3504. return true;
  3505. }
  3506. @Override
  3507. public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
  3508. if (selectedMessages == null || selectedMessages.size() < 1) {
  3509. mode.finish();
  3510. return true;
  3511. }
  3512. switch (item.getItemId()) {
  3513. case R.id.menu_message_copy:
  3514. copySelectedMessagesToClipboard();
  3515. mode.finish();
  3516. break;
  3517. case R.id.menu_message_discard:
  3518. deleteSelectedMessages();
  3519. break;
  3520. case R.id.menu_message_forward:
  3521. startForwardMessage();
  3522. mode.finish();
  3523. break;
  3524. case R.id.menu_message_ack:
  3525. sendUserAck();
  3526. mode.finish();
  3527. break;
  3528. case R.id.menu_message_dec:
  3529. sendUserDec();
  3530. mode.finish();
  3531. break;
  3532. case R.id.menu_message_save:
  3533. if (ConfigUtils.requestWriteStoragePermissions(activity, ComposeMessageFragment.this, PERMISSION_REQUEST_SAVE_MESSAGE)) {
  3534. fileService.saveMedia(activity, coordinatorLayout, new CopyOnWriteArrayList<>(selectedMessages), false);
  3535. }
  3536. mode.finish();
  3537. break;
  3538. case R.id.menu_message_log:
  3539. showMessageLog();
  3540. mode.finish();
  3541. break;
  3542. case R.id.menu_message_qrcode:
  3543. showAsQrCode(((ThreemaToolbarActivity) activity).getToolbar());
  3544. mode.finish();
  3545. break;
  3546. case R.id.menu_share:
  3547. shareMessages();
  3548. mode.finish();
  3549. break;
  3550. case R.id.menu_message_quote:
  3551. startQuoteMode(null, null);
  3552. mode.finish();
  3553. break;
  3554. case R.id.menu_show_text:
  3555. showTextChatBubble();
  3556. mode.finish();
  3557. break;
  3558. default:
  3559. return false;
  3560. }
  3561. return true;
  3562. }
  3563. @Override
  3564. public void onDestroyActionMode(ActionMode mode) {
  3565. actionMode = null;
  3566. longClickItem = AbsListView.INVALID_POSITION;
  3567. // handle done button
  3568. convListView.clearChoices();
  3569. convListView.requestLayout();
  3570. convListView.post(new Runnable() {
  3571. @Override
  3572. public void run() {
  3573. convListView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
  3574. }
  3575. });
  3576. }
  3577. }
  3578. private void showTextChatBubble() {
  3579. AbstractMessageModel messageModel = selectedMessages.get(0);
  3580. Intent intent = new Intent(getContext(), TextChatBubbleActivity.class);
  3581. IntentDataUtil.append(messageModel, intent);
  3582. activity.startActivity(intent);
  3583. activity.overridePendingTransition(R.anim.fast_fade_in, R.anim.fast_fade_out);
  3584. }
  3585. private void showAsQrCode(View v) {
  3586. AbstractMessageModel messageModel = selectedMessages.get(0);
  3587. if (messageModel != null && messageModel.getType() == MessageType.TEXT) {
  3588. new QRCodePopup(getContext(), getActivity().getWindow().getDecorView(), getActivity()).show(v, messageModel.getBody());
  3589. }
  3590. }
  3591. private void sendUserAck() {
  3592. messageService.sendUserAcknowledgement(selectedMessages.get(0));
  3593. Toast.makeText(getActivity(), R.string.message_acknowledged, Toast.LENGTH_SHORT).show();
  3594. }
  3595. /**
  3596. * Send a Decline Message
  3597. */
  3598. private void sendUserDec() {
  3599. messageService.sendUserDecline(selectedMessages.get(0));
  3600. Toast.makeText(getActivity(), R.string.message_declined, Toast.LENGTH_SHORT).show();
  3601. }
  3602. public boolean onBackPressed() {
  3603. logger.debug("onBackPressed");
  3604. // dismiss emoji keyboard if it's showing instead of leaving activity
  3605. if (emojiPicker != null && emojiPicker.isShown()) {
  3606. emojiPicker.hide();
  3607. return true;
  3608. } else {
  3609. if (mentionPopup != null && mentionPopup.isShowing()) {
  3610. dismissMentionPopup();
  3611. return true;
  3612. }
  3613. if (searchActionMode != null) {
  3614. searchActionMode.finish();
  3615. return true;
  3616. }
  3617. if (actionMode != null) {
  3618. actionMode.finish();
  3619. return true;
  3620. }
  3621. else if (ConfigUtils.isTabletLayout()) {
  3622. if (actionBar != null) {
  3623. actionBar.setDisplayUseLogoEnabled(true);
  3624. actionBar.setDisplayShowCustomEnabled(false);
  3625. }
  3626. }
  3627. return false;
  3628. }
  3629. }
  3630. private void preserveListInstanceValues() {
  3631. // this instance variable will probably survive an orientation change
  3632. // since setRetainInstance() is set in onCreate()
  3633. // so we don't put it into a bundle in onSaveInstanceState
  3634. listInstancePosition = AbsListView.INVALID_POSITION;
  3635. if (convListView != null && composeMessageAdapter != null) {
  3636. if (convListView.getLastVisiblePosition() != composeMessageAdapter.getCount() - 1) {
  3637. listInstancePosition = convListView.getFirstVisiblePosition();
  3638. View v = convListView.getChildAt(0);
  3639. listInstanceTop = (v == null) ? 0 : (v.getTop() - convListView.getPaddingTop());
  3640. if (messageReceiver != null) {
  3641. listInstanceReceiverId = messageReceiver.getUniqueIdString();
  3642. }
  3643. }
  3644. }
  3645. }
  3646. @Override
  3647. public void onSaveInstanceState(Bundle outState) {
  3648. logger.debug("onSaveInstanceState");
  3649. // some phones destroy the retained fragment upon going in background so we have to persist some data
  3650. outState.putParcelable(CAMERA_URI, cameraUri);
  3651. outState.putInt(ThreemaApplication.INTENT_DATA_GROUP, this.groupId);
  3652. outState.putInt(ThreemaApplication.INTENT_DATA_DISTRIBUTION_LIST, this.distributionListId);
  3653. outState.putString(ThreemaApplication.INTENT_DATA_CONTACT, this.identity);
  3654. super.onSaveInstanceState(outState);
  3655. }
  3656. private Integer getCurrentPageReferenceId() {
  3657. return this.currentPageReferenceId;
  3658. }
  3659. private void configureSearchWidget(final MenuItem menuItem) {
  3660. SearchView searchView = (SearchView) menuItem.getActionView();
  3661. if (searchView != null) {
  3662. searchView.setOnQueryTextListener(queryTextListener);
  3663. searchView.setQueryHint(getString(R.string.hint_search_keyword));
  3664. searchView.setIconified(false);
  3665. searchView.setOnCloseListener(() -> {
  3666. if (searchActionMode != null) {
  3667. searchActionMode.finish();
  3668. }
  3669. return false;
  3670. });
  3671. LinearLayout linearLayoutOfSearchView = (LinearLayout) searchView.getChildAt(0);
  3672. if (linearLayoutOfSearchView != null) {
  3673. linearLayoutOfSearchView.setGravity(Gravity.CENTER_VERTICAL);
  3674. linearLayoutOfSearchView.setPadding(0, 0, 0, 0);
  3675. searchCounter = (TextView) layoutInflater.inflate(R.layout.textview_search_action, null);
  3676. linearLayoutOfSearchView.addView(searchCounter);
  3677. FrameLayout searchPreviousLayout = (FrameLayout) layoutInflater.inflate(R.layout.button_search_action, null);
  3678. searchPreviousButton = searchPreviousLayout.findViewById(R.id.search_button);
  3679. searchPreviousButton.setImageDrawable(ConfigUtils.getThemedDrawable(activity, R.drawable.ic_keyboard_arrow_down_outline));
  3680. searchPreviousButton.setScaleY(-1);
  3681. searchPreviousButton.setOnClickListener(new View.OnClickListener() {
  3682. @Override
  3683. public void onClick(View v) {
  3684. composeMessageAdapter.previousMatchPosition();
  3685. }
  3686. });
  3687. linearLayoutOfSearchView.addView(searchPreviousLayout);
  3688. FrameLayout searchNextLayout = (FrameLayout) layoutInflater.inflate(R.layout.button_search_action, null);
  3689. searchNextButton = searchNextLayout.findViewById(R.id.search_button);
  3690. searchProgress = searchNextLayout.findViewById(R.id.next_progress);
  3691. searchNextButton.setImageDrawable(ConfigUtils.getThemedDrawable(activity, R.drawable.ic_keyboard_arrow_down_outline));
  3692. searchNextButton.setOnClickListener(new View.OnClickListener() {
  3693. @Override
  3694. public void onClick(View v) {
  3695. composeMessageAdapter.nextMatchPosition();
  3696. }
  3697. });
  3698. linearLayoutOfSearchView.addView(searchNextLayout);
  3699. }
  3700. }
  3701. }
  3702. private class SearchActionMode implements ActionMode.Callback {
  3703. @SuppressLint("StaticFieldLeak")
  3704. @Override
  3705. public boolean onCreateActionMode(ActionMode mode, final Menu menu) {
  3706. composeMessageAdapter.clearFilter();
  3707. activity.getMenuInflater().inflate(R.menu.action_compose_message_search, menu);
  3708. final MenuItem item = menu.findItem(R.id.menu_action_search);
  3709. final View actionView = item.getActionView();
  3710. item.setActionView(R.layout.item_progress);
  3711. item.expandActionView();
  3712. if (bottomPanel != null) {
  3713. bottomPanel.setVisibility(View.GONE);
  3714. }
  3715. if (emojiPicker != null && emojiPicker.isShown()) {
  3716. emojiPicker.hide();
  3717. }
  3718. dismissMentionPopup();
  3719. // load all records
  3720. new AsyncTask<Void, Void, Void>() {
  3721. List<AbstractMessageModel> messageModels;
  3722. @Override
  3723. protected Void doInBackground(Void... params) {
  3724. messageModels = getAllRecords();
  3725. return null;
  3726. }
  3727. @Override
  3728. protected void onPostExecute(Void result) {
  3729. if (messageModels != null && isAdded()) {
  3730. item.collapseActionView();
  3731. item.setActionView(actionView);
  3732. configureSearchWidget(menu.findItem(R.id.menu_action_search));
  3733. insertToList(messageModels, true, true, true);
  3734. convListView.setSelection(Integer.MAX_VALUE);
  3735. }
  3736. }
  3737. }.execute();
  3738. return true;
  3739. }
  3740. @Override
  3741. public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
  3742. return false;
  3743. }
  3744. @Override
  3745. public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
  3746. return false;
  3747. }
  3748. @Override
  3749. public void onDestroyActionMode(ActionMode mode) {
  3750. searchCounter = null;
  3751. searchActionMode = null;
  3752. if (composeMessageAdapter != null) {
  3753. composeMessageAdapter.clearFilter();
  3754. }
  3755. if (bottomPanel != null) {
  3756. bottomPanel.setVisibility(View.VISIBLE);
  3757. }
  3758. }
  3759. }
  3760. private void updateToolBarTitleInUIThread() {
  3761. RuntimeUtil.runOnUiThread(this::updateToolbarTitle);
  3762. }
  3763. @UiThread
  3764. private void updateContactModelData(final ContactModel contactModel) {
  3765. //update header
  3766. if(contactModel.getIdentity().equals(identity)) {
  3767. updateToolbarTitle();
  3768. }
  3769. if (composeMessageAdapter != null) {
  3770. composeMessageAdapter.resetCachedContactModelData(contactModel);
  3771. }
  3772. }
  3773. final protected boolean requiredInstances() {
  3774. if(!this.checkInstances()) {
  3775. this.instantiate();
  3776. }
  3777. return this.checkInstances();
  3778. }
  3779. protected boolean checkInstances() {
  3780. return TestUtil.required(
  3781. this.preferenceService,
  3782. this.userService,
  3783. this.contactService,
  3784. this.groupService,
  3785. this.messageService,
  3786. this.fileService,
  3787. this.notificationService,
  3788. this.distributionListService,
  3789. this.messagePlayerService,
  3790. this.blackListIdentityService,
  3791. this.ballotService,
  3792. this.conversationService,
  3793. this.deviceService,
  3794. this.wallpaperService,
  3795. this.mutedChatsListService,
  3796. this.ringtoneService,
  3797. this.voipStateService
  3798. );
  3799. }
  3800. protected void instantiate() {
  3801. ServiceManager serviceManager = ThreemaApplication.getServiceManager();
  3802. if (serviceManager != null) {
  3803. this.preferenceService = serviceManager.getPreferenceService();
  3804. try {
  3805. this.userService = serviceManager.getUserService();
  3806. this.contactService = serviceManager.getContactService();
  3807. this.groupService = serviceManager.getGroupService();
  3808. this.messageService = serviceManager.getMessageService();
  3809. this.fileService = serviceManager.getFileService();
  3810. this.notificationService = serviceManager.getNotificationService();
  3811. this.distributionListService = serviceManager.getDistributionListService();
  3812. this.messagePlayerService = serviceManager.getMessagePlayerService();
  3813. this.blackListIdentityService = serviceManager.getBlackListService();
  3814. this.ballotService = serviceManager.getBallotService();
  3815. this.databaseServiceNew = serviceManager.getDatabaseServiceNew();
  3816. this.conversationService = serviceManager.getConversationService();
  3817. this.deviceService =serviceManager.getDeviceService();
  3818. this.wallpaperService = serviceManager.getWallpaperService();
  3819. this.mutedChatsListService = serviceManager.getMutedChatsListService();
  3820. this.mentionOnlyChatsListService = serviceManager.getMentionOnlyChatsListService();
  3821. this.hiddenChatsListService = serviceManager.getHiddenChatsListService();
  3822. this.ringtoneService = serviceManager.getRingtoneService();
  3823. this.voipStateService = serviceManager.getVoipStateService();
  3824. this.downloadService = serviceManager.getDownloadService();
  3825. this.licenseService = serviceManager.getLicenseService();
  3826. } catch (Exception e) {
  3827. LogUtil.exception(e, activity);
  3828. }
  3829. }
  3830. }
  3831. // Dialog callbacks
  3832. @Override
  3833. public void onYes(String tag, Object data) {
  3834. switch (tag) {
  3835. case CONFIRM_TAG_DELETE_DISTRIBUTION_LIST:
  3836. final DistributionListModel dmodel = (DistributionListModel) data;
  3837. if (dmodel != null) {
  3838. new Thread(new Runnable() {
  3839. @Override
  3840. public void run() {
  3841. distributionListService.remove(dmodel);
  3842. RuntimeUtil.runOnUiThread(() -> activity.finish());
  3843. }
  3844. }).start();
  3845. }
  3846. break;
  3847. case ThreemaApplication.CONFIRM_TAG_CLOSE_BALLOT:
  3848. BallotUtil.closeBallot((AppCompatActivity) getActivity(), (BallotModel) data, ballotService);
  3849. break;
  3850. case DIALOG_TAG_CONFIRM_CALL:
  3851. VoipUtil.initiateCall((AppCompatActivity) getActivity(), contactModel, false, null);
  3852. break;
  3853. case DIALOG_TAG_EMPTY_CHAT:
  3854. emptyChat();
  3855. break;
  3856. case DIALOG_TAG_CONFIRM_BLOCK:
  3857. blackListIdentityService.toggle(activity, contactModel);
  3858. updateBlockMenu();
  3859. break;
  3860. case DIALOG_TAG_CONFIRM_LINK:
  3861. Uri uri = (Uri) data;
  3862. LinkifyUtil.getInstance().openLink(getContext(), uri);
  3863. break;
  3864. default:
  3865. break;
  3866. }
  3867. }
  3868. @Override
  3869. public void onNo(String tag, Object data) {
  3870. //
  3871. }
  3872. @Override
  3873. public void onEmojiPickerOpen() {
  3874. }
  3875. @Override
  3876. public void onEmojiPickerClose() {
  3877. }
  3878. @Override
  3879. public void onRequestPermissionsResult(int requestCode,
  3880. @NonNull String[] permissions, @NonNull int[] grantResults) {
  3881. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  3882. switch (requestCode) {
  3883. case PERMISSION_REQUEST_SAVE_MESSAGE:
  3884. fileService.saveMedia(activity, coordinatorLayout, new CopyOnWriteArrayList<>(selectedMessages), false);
  3885. break;
  3886. case PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE:
  3887. attachVoiceMessage();
  3888. break;
  3889. case PERMISSION_REQUEST_ATTACH_CAMERA:
  3890. updateCameraButton();
  3891. attachCamera();
  3892. break;
  3893. }
  3894. } else {
  3895. switch (requestCode) {
  3896. case PERMISSION_REQUEST_SAVE_MESSAGE:
  3897. if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) {
  3898. showPermissionRationale(R.string.permission_storage_required);
  3899. }
  3900. break;
  3901. case PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE:
  3902. if (!shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
  3903. showPermissionRationale(R.string.permission_record_audio_required);
  3904. }
  3905. break;
  3906. case PERMISSION_REQUEST_ATTACH_CAMERA:
  3907. case PERMISSION_REQUEST_ATTACH_CAMERA_VIDEO:
  3908. preferenceService.setCameraPermissionRequestShown(true);
  3909. if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
  3910. showPermissionRationale(R.string.permission_camera_photo_required);
  3911. }
  3912. updateCameraButton();
  3913. break;
  3914. }
  3915. }
  3916. }
  3917. /* properly dispose of popups */
  3918. private void dismissMentionPopup() {
  3919. if (this.mentionPopup != null) {
  3920. try {
  3921. this.mentionPopup.dismiss();
  3922. } catch( final IllegalArgumentException e){
  3923. // whatever
  3924. } finally{
  3925. this.mentionPopup = null;
  3926. }
  3927. }
  3928. }
  3929. private void dismissTooltipPopup(TooltipPopup tooltipPopup, boolean immediate) {
  3930. try {
  3931. if (tooltipPopup != null) {
  3932. tooltipPopup.dismiss(immediate);
  3933. }
  3934. } catch (final IllegalArgumentException e) {
  3935. // whatever
  3936. }
  3937. }
  3938. public void markAsRead() {
  3939. if (messageReceiver != null) {
  3940. try {
  3941. List<AbstractMessageModel> unreadMessages = messageReceiver.getUnreadMessages();
  3942. if (unreadMessages != null && unreadMessages.size() > 0) {
  3943. new Thread(new ReadMessagesRoutine(unreadMessages, this.messageService, this.notificationService)).start();
  3944. }
  3945. } catch (SQLException e) {
  3946. logger.error("Exception", e);
  3947. }
  3948. }
  3949. }
  3950. @Override
  3951. public void onConfigurationChanged(@NonNull Configuration newConfig) {
  3952. super.onConfigurationChanged(newConfig);
  3953. if (this.emojiPicker != null && this.emojiPicker.isShown()) {
  3954. this.emojiPicker.hide();
  3955. }
  3956. EditTextUtil.hideSoftKeyboard(this.messageText);
  3957. dismissMentionPopup();
  3958. dismissTooltipPopup(workTooltipPopup, true);
  3959. workTooltipPopup = null;
  3960. if (ConfigUtils.isTabletLayout()) {
  3961. // make sure layout changes after rotate are reflected in thumbnail size etc.
  3962. saveMessageDraft();
  3963. this.handleIntent(activity.getIntent());
  3964. } else {
  3965. if (isAdded()) {
  3966. // refresh wallpaper to reflect orientation change
  3967. this.wallpaperService.setupWallpaperBitmap(this.messageReceiver, this.wallpaperView, ConfigUtils.isLandscape(activity));
  3968. }
  3969. }
  3970. }
  3971. private void restoreMessageDraft() {
  3972. if (this.messageReceiver != null && this.messageText != null && TestUtil.empty(this.messageText.getText())) {
  3973. String messageDraft = ThreemaApplication.getMessageDraft(this.messageReceiver.getUniqueIdString());
  3974. if (!TextUtils.isEmpty(messageDraft)) {
  3975. this.messageText.setText("");
  3976. this.messageText.append(messageDraft);
  3977. } else {
  3978. this.messageText.setText("");
  3979. }
  3980. }
  3981. }
  3982. private void saveMessageDraft() {
  3983. if (this.messageReceiver != null) {
  3984. String draft = ThreemaApplication.getMessageDraft(this.messageReceiver.getUniqueIdString());
  3985. if (this.messageText.getText() != null) {
  3986. ThreemaApplication.putMessageDraft(this.messageReceiver.getUniqueIdString(), this.messageText.getText().toString());
  3987. }
  3988. if (!TestUtil.empty(this.messageText.getText()) || !TestUtil.empty(draft)) {
  3989. ListenerManager.conversationListeners.handle(new ListenerManager.HandleListener<ConversationListener>() {
  3990. @Override
  3991. public void handle(ConversationListener listener) {
  3992. listener.onModifiedAll();
  3993. }
  3994. });
  3995. }
  3996. }
  3997. }
  3998. @Override
  3999. public void onDismissed() {
  4000. updateMenus();
  4001. }
  4002. @Override
  4003. public void onKeyboardHidden() {
  4004. if (getActivity() != null && isAdded()) {
  4005. dismissMentionPopup();
  4006. dismissTooltipPopup(workTooltipPopup, false);
  4007. workTooltipPopup = null;
  4008. }
  4009. }
  4010. @Override
  4011. public void onKeyboardShown() {
  4012. if (emojiPicker != null && emojiPicker.isShown()) {
  4013. emojiPicker.hide();
  4014. }
  4015. }
  4016. }