ComposeMessageFragment.java 159 KB

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