MessageServiceImpl.java 206 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2013-2025 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.services;
  22. import android.annotation.SuppressLint;
  23. import android.app.Activity;
  24. import android.content.ActivityNotFoundException;
  25. import android.content.ClipData;
  26. import android.content.ContentResolver;
  27. import android.content.Context;
  28. import android.content.Intent;
  29. import android.database.Cursor;
  30. import android.database.sqlite.SQLiteException;
  31. import android.graphics.Bitmap;
  32. import android.graphics.BitmapFactory;
  33. import android.location.Location;
  34. import android.net.ConnectivityManager;
  35. import android.net.NetworkInfo;
  36. import android.net.Uri;
  37. import android.os.Build;
  38. import android.provider.DocumentsContract;
  39. import android.text.format.DateUtils;
  40. import android.util.SparseIntArray;
  41. import android.widget.Toast;
  42. import com.neilalexander.jnacl.NaCl;
  43. import org.apache.commons.io.IOUtils;
  44. import org.slf4j.Logger;
  45. import java.io.BufferedInputStream;
  46. import java.io.ByteArrayInputStream;
  47. import java.io.ByteArrayOutputStream;
  48. import java.io.File;
  49. import java.io.FileInputStream;
  50. import java.io.IOException;
  51. import java.io.InputStream;
  52. import java.lang.ref.WeakReference;
  53. import java.nio.charset.StandardCharsets;
  54. import java.security.SecureRandom;
  55. import java.sql.SQLException;
  56. import java.util.ArrayList;
  57. import java.util.Arrays;
  58. import java.util.Collection;
  59. import java.util.Collections;
  60. import java.util.Date;
  61. import java.util.HashMap;
  62. import java.util.Iterator;
  63. import java.util.List;
  64. import java.util.Map;
  65. import java.util.Objects;
  66. import java.util.Set;
  67. import java.util.concurrent.CopyOnWriteArrayList;
  68. import androidx.annotation.AnyThread;
  69. import androidx.annotation.NonNull;
  70. import androidx.annotation.Nullable;
  71. import androidx.annotation.WorkerThread;
  72. import androidx.collection.ArrayMap;
  73. import androidx.core.app.NotificationManagerCompat;
  74. import ch.threema.app.ExecutorServices;
  75. import ch.threema.app.R;
  76. import ch.threema.app.ThreemaApplication;
  77. import ch.threema.app.collections.Functional;
  78. import ch.threema.app.collections.IPredicateNonNull;
  79. import ch.threema.app.emojis.EmojiUtil;
  80. import ch.threema.app.exceptions.NotAllowedException;
  81. import ch.threema.app.exceptions.TranscodeCanceledException;
  82. import ch.threema.app.managers.ListenerManager;
  83. import ch.threema.app.messagereceiver.ContactMessageReceiver;
  84. import ch.threema.app.messagereceiver.DistributionListMessageReceiver;
  85. import ch.threema.app.messagereceiver.GroupMessageReceiver;
  86. import ch.threema.app.messagereceiver.MessageReceiver;
  87. import ch.threema.app.multidevice.MultiDeviceManager;
  88. import ch.threema.app.notifications.NotificationIDs;
  89. import ch.threema.app.preference.service.PreferenceService;
  90. import ch.threema.app.routines.MarkAsReadRoutine;
  91. import ch.threema.app.services.ballot.BallotService;
  92. import ch.threema.app.services.ballot.BallotUpdateResult;
  93. import ch.threema.app.services.messageplayer.MessagePlayerService;
  94. import ch.threema.app.services.notification.NotificationService;
  95. import ch.threema.app.stores.IdentityStore;
  96. import ch.threema.app.ui.MediaItem;
  97. import ch.threema.app.utils.BallotUtil;
  98. import ch.threema.app.utils.BitmapUtil;
  99. import ch.threema.app.utils.ConfigUtils;
  100. import ch.threema.app.utils.ContactUtil;
  101. import ch.threema.app.utils.ExifInterface;
  102. import ch.threema.app.utils.FileUtil;
  103. import ch.threema.app.utils.GeoLocationUtil;
  104. import ch.threema.app.utils.GroupUtil;
  105. import ch.threema.app.utils.IconUtil;
  106. import ch.threema.app.utils.MessageUtil;
  107. import ch.threema.app.utils.MimeUtil;
  108. import ch.threema.app.utils.NameUtil;
  109. import ch.threema.app.utils.QuoteUtil;
  110. import ch.threema.app.utils.RuntimeUtil;
  111. import ch.threema.app.utils.StringConversionUtil;
  112. import ch.threema.app.utils.TestUtil;
  113. import ch.threema.app.utils.ThumbnailUtil;
  114. import ch.threema.app.utils.VideoUtil;
  115. import ch.threema.app.video.transcoder.VideoConfig;
  116. import ch.threema.app.video.transcoder.VideoTranscoder;
  117. import ch.threema.app.voip.groupcall.GroupCallDescription;
  118. import ch.threema.base.ProgressListener;
  119. import ch.threema.base.ThreemaException;
  120. import ch.threema.base.crypto.SymmetricEncryptionResult;
  121. import ch.threema.base.crypto.SymmetricEncryptionService;
  122. import ch.threema.base.utils.LoggingUtil;
  123. import ch.threema.base.utils.Utils;
  124. import ch.threema.data.models.EmojiReactionData;
  125. import ch.threema.data.repositories.EditHistoryRepository;
  126. import ch.threema.data.repositories.EmojiReactionEntryCreateException;
  127. import ch.threema.data.repositories.EmojiReactionEntryRemoveException;
  128. import ch.threema.data.repositories.EmojiReactionsRepository;
  129. import ch.threema.domain.models.GroupId;
  130. import ch.threema.domain.models.MessageId;
  131. import ch.threema.domain.protocol.blob.BlobScope;
  132. import ch.threema.domain.protocol.blob.BlobUploader;
  133. import ch.threema.domain.protocol.csp.MessageTooLongException;
  134. import ch.threema.domain.protocol.csp.ProtocolDefines;
  135. import ch.threema.domain.protocol.csp.messages.AbstractGroupMessage;
  136. import ch.threema.domain.protocol.csp.messages.AbstractMessage;
  137. import ch.threema.domain.protocol.csp.messages.AudioMessage;
  138. import ch.threema.domain.protocol.csp.messages.BadMessageException;
  139. import ch.threema.domain.protocol.csp.messages.DeleteMessage;
  140. import ch.threema.domain.protocol.csp.messages.GroupAudioMessage;
  141. import ch.threema.domain.protocol.csp.messages.GroupImageMessage;
  142. import ch.threema.domain.protocol.csp.messages.location.GroupLocationMessage;
  143. import ch.threema.domain.protocol.csp.messages.GroupTextMessage;
  144. import ch.threema.domain.protocol.csp.messages.GroupVideoMessage;
  145. import ch.threema.domain.protocol.csp.messages.ImageMessage;
  146. import ch.threema.domain.protocol.csp.messages.location.LocationMessage;
  147. import ch.threema.domain.protocol.csp.messages.TextMessage;
  148. import ch.threema.domain.protocol.csp.messages.VideoMessage;
  149. import ch.threema.domain.protocol.csp.messages.ballot.BallotSetupInterface;
  150. import ch.threema.domain.protocol.csp.messages.ballot.GroupPollSetupMessage;
  151. import ch.threema.domain.protocol.csp.messages.ballot.PollSetupMessage;
  152. import ch.threema.domain.protocol.csp.messages.file.FileData;
  153. import ch.threema.domain.protocol.csp.messages.fs.ForwardSecurityMode;
  154. import ch.threema.domain.protocol.csp.messages.location.Poi;
  155. import ch.threema.domain.taskmanager.TriggerSource;
  156. import ch.threema.protobuf.csp.e2e.Reaction;
  157. import ch.threema.storage.DatabaseService;
  158. import ch.threema.storage.factories.GroupMessageModelFactory;
  159. import ch.threema.storage.factories.MessageModelFactory;
  160. import ch.threema.storage.models.AbstractMessageModel;
  161. import ch.threema.storage.models.ContactModel;
  162. import ch.threema.storage.models.DistributionListMessageModel;
  163. import ch.threema.storage.models.FirstUnreadMessageModel;
  164. import ch.threema.storage.models.GroupMessageModel;
  165. import ch.threema.storage.models.GroupModel;
  166. import ch.threema.storage.models.MessageModel;
  167. import ch.threema.storage.models.MessageState;
  168. import ch.threema.storage.models.MessageType;
  169. import ch.threema.storage.models.ServerMessageModel;
  170. import ch.threema.storage.models.access.GroupAccessModel;
  171. import ch.threema.storage.models.ballot.BallotModel;
  172. import ch.threema.storage.models.data.LocationDataModel;
  173. import ch.threema.storage.models.data.MessageContentsType;
  174. import ch.threema.storage.models.data.media.AudioDataModel;
  175. import ch.threema.storage.models.data.media.BallotDataModel;
  176. import ch.threema.storage.models.data.media.FileDataModel;
  177. import ch.threema.storage.models.data.media.ImageDataModel;
  178. import ch.threema.storage.models.data.media.MediaMessageDataInterface;
  179. import ch.threema.storage.models.data.media.VideoDataModel;
  180. import ch.threema.storage.models.data.status.ForwardSecurityStatusDataModel;
  181. import ch.threema.storage.models.data.status.GroupCallStatusDataModel;
  182. import ch.threema.storage.models.data.status.GroupStatusDataModel;
  183. import ch.threema.storage.models.data.status.VoipStatusDataModel;
  184. import static ch.threema.app.AppConstants.MAX_BLOB_SIZE;
  185. import static ch.threema.app.AppConstants.MAX_BLOB_SIZE_MB;
  186. import static ch.threema.app.preference.service.PreferenceService.ImageScale_DEFAULT;
  187. import static ch.threema.app.ui.MediaItem.TIME_UNDEFINED;
  188. import static ch.threema.app.ui.MediaItem.TYPE_FILE;
  189. import static ch.threema.app.ui.MediaItem.TYPE_IMAGE;
  190. import static ch.threema.app.ui.MediaItem.TYPE_IMAGE_ANIMATED;
  191. import static ch.threema.app.ui.MediaItem.TYPE_IMAGE_CAM;
  192. import static ch.threema.app.ui.MediaItem.TYPE_LOCATION;
  193. import static ch.threema.app.ui.MediaItem.TYPE_TEXT;
  194. import static ch.threema.app.ui.MediaItem.TYPE_VIDEO;
  195. import static ch.threema.app.ui.MediaItem.TYPE_VIDEO_CAM;
  196. import static ch.threema.app.ui.MediaItem.TYPE_VOICEMESSAGE;
  197. import static ch.threema.app.utils.MessageUtilKt.canBeEdited;
  198. import static ch.threema.app.utils.StreamUtilKt.getFromUri;
  199. import static ch.threema.domain.protocol.csp.messages.file.FileData.RENDERING_STICKER;
  200. public class MessageServiceImpl implements MessageService {
  201. private static final Logger logger = LoggingUtil.getThreemaLogger("MessageServiceImpl");
  202. public static final long FILE_AUTO_DOWNLOAD_MAX_SIZE_M = 5; // MB
  203. public static final long FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO = FILE_AUTO_DOWNLOAD_MAX_SIZE_M * 1024 * 1024; // used for calculations
  204. public static final long FILE_AUTO_DOWNLOAD_MAX_SIZE_SI = FILE_AUTO_DOWNLOAD_MAX_SIZE_M * 1000 * 1000; // used for presentation only
  205. public static final int THUMBNAIL_SIZE_PX = 512;
  206. private final @NonNull Context context;
  207. // Services
  208. private final MessageSendingService messageSendingService;
  209. private final DatabaseService databaseService;
  210. private final ContactService contactService;
  211. private final FileService fileService;
  212. private final IdentityStore identityStore;
  213. private final BallotService ballotService;
  214. private final PreferenceService preferenceService;
  215. private final LockAppService appLockService;
  216. private final GroupService groupService;
  217. private final ApiService apiService;
  218. private final DownloadService downloadService;
  219. private final ConversationCategoryService conversationCategoryService;
  220. @NonNull
  221. private final BlockedIdentitiesService blockedIdentitiesService;
  222. private final SymmetricEncryptionService symmetricEncryptionService;
  223. // Repositories
  224. private final EditHistoryRepository editHistoryRepository;
  225. private final EmojiReactionsRepository emojiReactionsRepository;
  226. // Managers
  227. private final MultiDeviceManager multiDeviceManager;
  228. // Caches
  229. private final Collection<MessageModel> contactMessageCache;
  230. private final Collection<GroupMessageModel> groupMessageCache;
  231. private final Collection<DistributionListMessageModel> distributionListMessageCache;
  232. private final SparseIntArray loadingProgress = new SparseIntArray();
  233. public MessageServiceImpl(
  234. @NonNull
  235. Context context,
  236. CacheService cacheService,
  237. DatabaseService databaseService,
  238. ContactService contactService,
  239. FileService fileService,
  240. IdentityStore identityStore,
  241. SymmetricEncryptionService symmetricEncryptionService,
  242. PreferenceService preferenceService,
  243. LockAppService appLockService,
  244. BallotService ballotService,
  245. GroupService groupService,
  246. ApiService apiService,
  247. DownloadService downloadService,
  248. @NonNull ConversationCategoryService conversationCategoryService,
  249. @NonNull BlockedIdentitiesService blockedIdentitiesService,
  250. MultiDeviceManager multiDeviceManager,
  251. EditHistoryRepository editHistoryRepository,
  252. EmojiReactionsRepository emojiReactionsRepository
  253. ) {
  254. this.context = context;
  255. this.databaseService = databaseService;
  256. this.contactService = contactService;
  257. this.fileService = fileService;
  258. this.identityStore = identityStore;
  259. this.symmetricEncryptionService = symmetricEncryptionService;
  260. this.preferenceService = preferenceService;
  261. this.appLockService = appLockService;
  262. this.ballotService = ballotService;
  263. this.groupService = groupService;
  264. this.apiService = apiService;
  265. this.downloadService = downloadService;
  266. this.conversationCategoryService = conversationCategoryService;
  267. this.blockedIdentitiesService = blockedIdentitiesService;
  268. contactMessageCache = cacheService.getMessageModelCache();
  269. groupMessageCache = cacheService.getGroupMessageModelCache();
  270. distributionListMessageCache = cacheService.getDistributionListMessageCache();
  271. this.multiDeviceManager = multiDeviceManager;
  272. this.editHistoryRepository = editHistoryRepository;
  273. this.emojiReactionsRepository = emojiReactionsRepository;
  274. // init queue
  275. messageSendingService = new MessageSendingServiceExponentialBackOff(new MessageSendingService.MessageSendingServiceState() {
  276. @Override
  277. public void processingFailed(AbstractMessageModel messageModel, MessageReceiver<AbstractMessageModel> receiver) {
  278. //remove send machine
  279. removeSendMachine(messageModel);
  280. updateOutgoingMessageState(messageModel, MessageState.SENDFAILED, new Date());
  281. }
  282. @Override
  283. public void exception(Exception x, int tries) {
  284. if (tries >= 5) {
  285. logger.error("Exception", x);
  286. }
  287. }
  288. });
  289. }
  290. private void cache(AbstractMessageModel m) {
  291. if (m instanceof GroupMessageModel) {
  292. synchronized (groupMessageCache) {
  293. groupMessageCache.add((GroupMessageModel) m);
  294. }
  295. } else if (m instanceof MessageModel) {
  296. synchronized (contactMessageCache) {
  297. contactMessageCache.add((MessageModel) m);
  298. }
  299. }
  300. }
  301. @Override
  302. public AbstractMessageModel createStatusMessage(String statusMessage, MessageReceiver receiver) {
  303. AbstractMessageModel model = receiver.createAndSaveStatusModel(statusMessage, new Date());
  304. fireOnCreatedMessage(model);
  305. return model;
  306. }
  307. @Override
  308. public AbstractMessageModel createVoipStatus(
  309. @NonNull VoipStatusDataModel data,
  310. @NonNull MessageReceiver receiver,
  311. boolean isOutbox,
  312. boolean isRead
  313. ) {
  314. logger.info("Storing voip status message (outbox={}, status={}, reason={})",
  315. isOutbox, data.getStatus(), data.getReason());
  316. final AbstractMessageModel model = receiver.createLocalModel(
  317. MessageType.VOIP_STATUS,
  318. MessageContentsType.VOIP_STATUS,
  319. data.getDate() != null ? data.getDate() : new Date()
  320. );
  321. model.setOutbox(isOutbox);
  322. model.setVoipStatusData(data);
  323. model.setSaved(true);
  324. model.setRead(isRead);
  325. receiver.saveLocalModel(model);
  326. fireOnCreatedMessage(model);
  327. return model;
  328. }
  329. @Override
  330. @Nullable
  331. public AbstractMessageModel createGroupCallStatus(
  332. @NonNull GroupCallStatusDataModel data,
  333. @NonNull MessageReceiver receiver,
  334. @Nullable ContactModel callerContactModel,
  335. @Nullable GroupCallDescription call,
  336. boolean isOutbox,
  337. Date postedDate) {
  338. if (receiver instanceof GroupMessageReceiver && ((GroupMessageReceiver) receiver).getGroup() == null) {
  339. logger.info("Unable to store group call status message. Group no longer exists");
  340. return null;
  341. }
  342. logger.info("Storing group call status message for call={}", call != null ? call.getCallId() : "n/a");
  343. final AbstractMessageModel model = receiver.createLocalModel(
  344. MessageType.GROUP_CALL_STATUS,
  345. MessageContentsType.GROUP_CALL_STATUS,
  346. new Date()
  347. );
  348. model.setPostedAt(postedDate);
  349. model.setOutbox(isOutbox);
  350. model.setGroupCallStatusData(data);
  351. model.setSaved(true);
  352. model.setStatusMessage(true);
  353. model.setRead(data.getStatus() != GroupCallStatusDataModel.STATUS_STARTED);
  354. receiver.saveLocalModel(model);
  355. fireOnCreatedMessage(model);
  356. return model;
  357. }
  358. @Override
  359. public AbstractMessageModel createForwardSecurityStatus(
  360. @NonNull MessageReceiver receiver,
  361. @ForwardSecurityStatusDataModel.ForwardSecurityStatusType int type,
  362. int quantity,
  363. @Nullable String staticText) {
  364. logger.info("Storing forward security status message of type {}", type);
  365. final AbstractMessageModel model = receiver.createLocalModel(
  366. MessageType.FORWARD_SECURITY_STATUS,
  367. MessageContentsType.FORWARD_SECURITY_STATUS,
  368. new Date()
  369. );
  370. model.setOutbox(false);
  371. model.setForwardSecurityStatusData(ForwardSecurityStatusDataModel.create(type, quantity, staticText));
  372. model.setSaved(true);
  373. model.setStatusMessage(true);
  374. model.setRead(true);
  375. receiver.saveLocalModel(model);
  376. fireOnCreatedMessage(model);
  377. return model;
  378. }
  379. @Override
  380. public AbstractMessageModel createGroupStatus(
  381. @NonNull GroupMessageReceiver receiver,
  382. @NonNull GroupStatusDataModel.GroupStatusType type,
  383. @Nullable String identity,
  384. @Nullable String ballotName,
  385. @Nullable String newGroupName
  386. ) {
  387. logger.info("Storing group status message of type {}", type.getType());
  388. final GroupMessageModel model = receiver.createLocalModel(
  389. MessageType.GROUP_STATUS,
  390. MessageContentsType.GROUP_STATUS,
  391. new Date()
  392. );
  393. model.setOutbox(false);
  394. model.setGroupStatusData(GroupStatusDataModel.create(type, identity, ballotName, newGroupName));
  395. model.setSaved(true);
  396. model.setStatusMessage(true);
  397. model.setRead(true);
  398. receiver.saveLocalModel(model);
  399. fireOnCreatedMessage(model);
  400. return model;
  401. }
  402. public AbstractMessageModel createNewBallotMessage(
  403. MessageId messageId,
  404. BallotModel ballotModel,
  405. BallotDataModel.Type type,
  406. MessageReceiver receiver,
  407. int messageFlags,
  408. ForwardSecurityMode forwardSecurityMode) {
  409. AbstractMessageModel model = receiver.createLocalModel(MessageType.BALLOT, MessageContentsType.BALLOT, new Date());
  410. if (model != null) {
  411. //hack: save ballot id into body string
  412. model.setIdentity(ballotModel.getCreatorIdentity());
  413. model.setSaved(true);
  414. model.setBallotData(new BallotDataModel(type, ballotModel.getId()));
  415. model.setOutbox(ballotModel.getCreatorIdentity().equals(identityStore.getIdentity()));
  416. model.setApiMessageId(messageId.toString());
  417. model.setMessageFlags(messageFlags);
  418. model.setForwardSecurityMode(forwardSecurityMode);
  419. receiver.saveLocalModel(model);
  420. cache(model);
  421. fireOnCreatedMessage(model);
  422. }
  423. return model;
  424. }
  425. /**
  426. * Send a text message to the specified receiver.
  427. *
  428. * @param message The message text. May not be longer than {@link ProtocolDefines#MAX_TEXT_MESSAGE_LEN} UTF-8 bytes.
  429. * @param messageReceiver The receiver for this message.
  430. * @return the model of the sent message
  431. * @throws MessageTooLongException if the message is too long.
  432. * @throws ThreemaException if the message text is empty after trimming.
  433. */
  434. @Override
  435. public AbstractMessageModel sendText(
  436. @NonNull String message,
  437. @NonNull MessageReceiver messageReceiver
  438. ) throws ThreemaException {
  439. final String tag = "sendTextMessage";
  440. logger.info("{}: start", tag);
  441. String trimmedMessage = validateTextMessage(message);
  442. logger.debug("{}: create model instance", tag);
  443. final AbstractMessageModel messageModel = messageReceiver.createLocalModel(MessageType.TEXT, MessageContentsType.TEXT, new Date());
  444. logger.debug("{}: cache", tag);
  445. cache(messageModel);
  446. messageModel.setOutbox(true);
  447. messageModel.setBodyAndQuotedMessageId(trimmedMessage);
  448. messageModel.setState(MessageState.SENDING);
  449. messageModel.setSaved(true);
  450. logger.debug("{}: save db", tag);
  451. messageReceiver.saveLocalModel(messageModel);
  452. logger.debug("{}: fire create message", tag);
  453. fireOnCreatedMessage(messageModel);
  454. messageReceiver.createAndSendTextMessage(messageModel);
  455. String messageId = messageModel.getApiMessageId();
  456. logger.info("{}: message {} successfully queued", tag, (messageId != null ? messageId : messageModel.getId()));
  457. messageReceiver.saveLocalModel(messageModel);
  458. fireOnModifiedMessage(messageModel);
  459. return messageModel;
  460. }
  461. @Override
  462. public void sendEditedMessageText(
  463. @NonNull AbstractMessageModel message, // Let `message` be the referred message.
  464. @NonNull String newText,
  465. @NonNull Date editedAt,
  466. @NonNull MessageReceiver receiver
  467. ) throws ThreemaException {
  468. logger.debug("editText message = {}", message.getApiMessageId());
  469. if (!message.isOutbox()) {
  470. throw new ThreemaException("Tried editing a message that is not outgoing. message = " + message.getApiMessageId());
  471. }
  472. String trimmedNewText = validateTextMessage(newText);
  473. if (Objects.equals(message.getBody(), trimmedNewText)) {
  474. throw new ThreemaException("Tried editing a message with no changes. message = " + message.getApiMessageId());
  475. }
  476. if (message.getPostedAt() == null) {
  477. logger.error("postedAt is null for messageId={}}", message.getId());
  478. return;
  479. }
  480. if (!canBeEdited(message, isNotesGroup(receiver), editedAt, AbstractMessageModel::getPostedAt)) {
  481. logger.error("Message can not be edited");
  482. return;
  483. }
  484. if (receiver instanceof ContactMessageReceiver) {
  485. ((ContactMessageReceiver) receiver).sendEditMessage(
  486. message.getId(),
  487. trimmedNewText,
  488. editedAt
  489. );
  490. } else if (receiver instanceof GroupMessageReceiver) {
  491. ((GroupMessageReceiver) receiver).sendEditMessage(
  492. message.getId(),
  493. trimmedNewText,
  494. editedAt
  495. );
  496. } else {
  497. throw new ThreemaException("Unsupported receiver type of: " + receiver.getClass());
  498. }
  499. saveEditedMessageText(message, newText, editedAt);
  500. }
  501. private boolean isNotesGroup(@NonNull MessageReceiver receiver) {
  502. if (receiver instanceof GroupMessageReceiver) {
  503. return groupService.isNotesGroup(((GroupMessageReceiver) receiver).getGroup());
  504. }
  505. return false;
  506. }
  507. @Override
  508. public void saveEditedMessageText(@NonNull AbstractMessageModel message, String text, @Nullable Date editedAt) {
  509. logger.info("Save edited message = {}", message.getApiMessageId());
  510. if (editedAt != null) {
  511. editHistoryRepository.createEntry(message);
  512. }
  513. // Edit `message` as defined by the associated _Edit applies to_ property and
  514. // add an indicator to `message`, informing the user that the message has
  515. // been edited by the user at `created-at`.
  516. switch (message.getType()) {
  517. case TEXT:
  518. message.setBody(text);
  519. break;
  520. case FILE:
  521. message.setCaption(text);
  522. message.getFileData().setCaption(text);
  523. message.setBody(message.getFileData().toString());
  524. break;
  525. default:
  526. logger.error("Tried saving an edited message of unsupported type {} for messageId = {}}", message.getType(), message.getId());
  527. return;
  528. }
  529. message.setEditedAt(editedAt);
  530. save(message);
  531. fireOnModifiedMessage(message);
  532. fireOnEditMessage(message);
  533. }
  534. @Override
  535. public boolean saveEmojiReactionMessage(
  536. @NonNull AbstractMessageModel targetMessage,
  537. @NonNull String senderIdentity,
  538. @Nullable Reaction.ActionCase actionCase,
  539. @NonNull String emojiSequence
  540. ) {
  541. logger.debug("saving emoji reaction of type {} to message {}", actionCase, targetMessage.getApiMessageId());
  542. if (actionCase == Reaction.ActionCase.APPLY) {
  543. try {
  544. emojiReactionsRepository.createEntry(targetMessage, senderIdentity, emojiSequence);
  545. } catch (EmojiReactionEntryCreateException | IllegalStateException e) {
  546. logger.error("Unable to create emoji reaction.", e);
  547. return false;
  548. }
  549. } else if (actionCase == Reaction.ActionCase.WITHDRAW) {
  550. try {
  551. emojiReactionsRepository.removeEntry(targetMessage, senderIdentity, emojiSequence);
  552. } catch (EmojiReactionEntryRemoveException | IllegalStateException e) {
  553. logger.error("Unable to remove emoji reaction.", e);
  554. return false;
  555. }
  556. } else {
  557. logger.warn("Unsupported emoji reaction action case {}. Ignoring message.", actionCase);
  558. return false;
  559. }
  560. fireOnModifiedMessage(targetMessage);
  561. return true;
  562. }
  563. @Override
  564. public void clearMessageState(@NonNull AbstractMessageModel targetMessage) {
  565. if (targetMessage.getState() != MessageState.USERACK && targetMessage.getState() != MessageState.USERDEC) {
  566. return;
  567. }
  568. MessageState newMessageState;
  569. String myIdentity = identityStore != null ? identityStore.getIdentity() : null;
  570. if (targetMessage.isRead()) {
  571. newMessageState = MessageState.READ;
  572. } else if (targetMessage.getDeliveredAt() != null) {
  573. newMessageState = MessageState.DELIVERED;
  574. } else {
  575. newMessageState = MessageState.SENT;
  576. }
  577. targetMessage.setState(newMessageState);
  578. if (targetMessage instanceof GroupMessageModel && myIdentity != null) {
  579. groupService.removeGroupMessageState((GroupMessageModel) targetMessage, myIdentity);
  580. }
  581. save(targetMessage);
  582. }
  583. @WorkerThread
  584. @Override
  585. public synchronized boolean sendEmojiReaction(
  586. @NonNull AbstractMessageModel message,
  587. @NonNull String emojiSequence,
  588. @NonNull MessageReceiver receiver,
  589. boolean markAsRead
  590. ) throws ThreemaException {
  591. logger.debug("Send emoji reaction to message {} (id={})", message.getApiMessageId(), message.getId());
  592. logger.trace("Reaction: '{}'", emojiSequence);
  593. if (!EmojiUtil.isFullyQualifiedEmoji(emojiSequence)) {
  594. logger.warn("Attempt to send non fully-qualified emoji sequence '{}'", emojiSequence);
  595. // Return true, as the return value only indicates whether this failed due to
  596. // compatibility issues when a phase 1 client tries to send an emoji sequence
  597. // to a client without reactions support.
  598. return true;
  599. }
  600. @MessageReceiver.EmojiReactionsSupport final int reactionSupport = receiver.getEmojiReactionSupport();
  601. if (markAsRead) {
  602. markAsRead(
  603. /* message */ message,
  604. /* silent */ true
  605. );
  606. }
  607. final String myIdentity = identityStore.getIdentity();
  608. List<EmojiReactionData> emojiReactionData =
  609. emojiReactionsRepository.safeGetReactionsByMessage(message);
  610. Reaction.ActionCase actionCase = Reaction.ActionCase.APPLY;
  611. // check if there's already an identical reaction with us as the sender. if yes, withdraw
  612. // it.
  613. if (containsEmojiSequence(emojiReactionData, emojiSequence, identityStore.getIdentity())) {
  614. actionCase = Reaction.ActionCase.WITHDRAW;
  615. }
  616. // If there is a new message state set, it means that a legacy reaction was sent and the
  617. // state of the message needs to be updated.
  618. MessageState newMessageState = null;
  619. if (receiver instanceof ContactMessageReceiver) {
  620. newMessageState = ((ContactMessageReceiver) receiver).sendReaction(
  621. message,
  622. actionCase,
  623. emojiSequence,
  624. new Date() // use current timestamp for reaction message
  625. );
  626. } else if (receiver instanceof GroupMessageReceiver) {
  627. ((GroupMessageReceiver) receiver).sendReaction(
  628. message,
  629. actionCase,
  630. emojiSequence,
  631. new Date() // use current timestamp for reaction message
  632. );
  633. } else {
  634. throw new ThreemaException("Unsupported receiver type of: " + receiver.getClass());
  635. }
  636. if (newMessageState == null) {
  637. // In case the new message state is null, then an emoji reaction has been sent. The
  638. // sequence can be stored normally.
  639. if (actionCase == Reaction.ActionCase.APPLY) {
  640. emojiReactionsRepository.createEntry(message, myIdentity, emojiSequence);
  641. } else {
  642. emojiReactionsRepository.removeEntry(message, myIdentity, emojiSequence);
  643. }
  644. } else {
  645. // In case there is a new message state, then a legacy reaction has been used. In this
  646. // case we need to update the message state.
  647. updateAckDecState(message, newMessageState, null);
  648. }
  649. showToastOnPartialReactionSupport(
  650. reactionSupport,
  651. actionCase,
  652. emojiSequence
  653. );
  654. fireOnModifiedMessage(message);
  655. return true;
  656. }
  657. /**
  658. * Show a toast if reaction support is only partial for the current receiver.
  659. * Also note that for now a toast is only shown if the action is APPLY.
  660. * If the reaction can be mapped to ACK/DEC, no toast is shown.
  661. */
  662. @AnyThread
  663. private void showToastOnPartialReactionSupport(
  664. @MessageReceiver.EmojiReactionsSupport int reactionSupport,
  665. @NonNull Reaction.ActionCase actionCase,
  666. @NonNull String emojiSequence
  667. ) {
  668. if (reactionSupport == MessageReceiver.Reactions_PARTIAL
  669. && actionCase == Reaction.ActionCase.APPLY
  670. && !EmojiUtil.isThumbsUpOrDownEmoji(emojiSequence)) {
  671. RuntimeUtil.runOnUiThread(() ->
  672. Toast.makeText(context, R.string.group_emoji_reactions_partially_supported, Toast.LENGTH_SHORT).show());
  673. }
  674. }
  675. private boolean containsEmojiSequence(@Nullable List<EmojiReactionData> emojiReactionData, @NonNull String emojiSequence, @NonNull String senderIdentity) {
  676. return
  677. emojiReactionData != null &&
  678. emojiReactionData.stream().anyMatch(a -> a.senderIdentity.equals(senderIdentity) && a.emojiSequence.equals(emojiSequence));
  679. }
  680. @Override
  681. public void sendDeleteMessage(
  682. @NonNull AbstractMessageModel message, // Let `message` be the referred message.
  683. @NonNull MessageReceiver receiver
  684. ) throws Exception {
  685. logger.debug("sendDeleteMessage message = {}", message.getApiMessageId());
  686. if (!message.isOutbox()) {
  687. logger.error("Tried deleting a message that is not outgoing. message = {}", message.getId());
  688. }
  689. if (message.getPostedAt() == null) {
  690. logger.error("postedAt is null for messageId={}}", message.getId());
  691. return;
  692. }
  693. // 3. Let `created-at` be the current timestamp to be applied to the delete
  694. // message.
  695. Date createdAt = new Date();
  696. long deltaTime = createdAt.getTime() - message.getPostedAt().getTime();
  697. // 2. If the referred message has been sent (`sent-at`) more than 6 hours ago,
  698. // prevent creation and abort these steps.
  699. if (deltaTime > DeleteMessage.DELETE_MESSAGES_MAX_AGE) {
  700. logger.error("Cannot delete message older than {}}ms", DeleteMessage.DELETE_MESSAGES_MAX_AGE);
  701. }
  702. // 4. Replace `message` with a message informing the user that the message of
  703. // the user has been removed at `created-at`.
  704. deleteMessageContentsAndRelatedData(message, createdAt);
  705. if (receiver instanceof ContactMessageReceiver) {
  706. ((ContactMessageReceiver) receiver).sendDeleteMessage(
  707. message.getId(),
  708. createdAt
  709. );
  710. } else if (receiver instanceof GroupMessageReceiver) {
  711. ((GroupMessageReceiver) receiver).sendDeleteMessage(
  712. message.getId(),
  713. createdAt
  714. );
  715. } else {
  716. throw new ThreemaException("Unsupported receiver type of: " + receiver.getClass());
  717. }
  718. }
  719. @Override
  720. public void deleteMessageContentsAndRelatedData(@NonNull AbstractMessageModel message, Date deletedAt) {
  721. logger.info("deleteMessageContents = {}", message.getApiMessageId());
  722. fileService.removeMessageFiles(message, true);
  723. message.setBody(null);
  724. message.setCaption(null);
  725. message.setState(null);
  726. if (message instanceof GroupMessageModel) {
  727. ((GroupMessageModel) message).setGroupMessageStates(null);
  728. }
  729. message.setDeletedAt(deletedAt);
  730. save(message);
  731. // Delete the edit history and emoji reactions. Note that the foreign keys do not work in this case, as the
  732. // original message entry is not removed from the database.
  733. editHistoryRepository.deleteByMessageUid(message.getUid());
  734. emojiReactionsRepository.deleteAllReactionsForMessage(message);
  735. fireOnModifiedMessage(message);
  736. fireOnMessageDeletedForAll(message);
  737. }
  738. @Override
  739. public AbstractMessageModel sendLocation(@NonNull Location location, @Nullable String poiName, MessageReceiver receiver, final CompletionHandler completionHandler) throws ThreemaException {
  740. final String tag = "sendLocationMessage";
  741. logger.info("{}: start", tag);
  742. AbstractMessageModel messageModel = receiver.createLocalModel(MessageType.LOCATION, MessageContentsType.LOCATION, new Date());
  743. cache(messageModel);
  744. @Nullable Poi poi = null;
  745. try {
  746. final @NonNull String lookedUpPoiAddress = GeoLocationUtil.getAddressFromLocation(
  747. context,
  748. location.getLatitude(),
  749. location.getLongitude()
  750. );
  751. if (poiName != null && !poiName.isBlank()) {
  752. poi = new Poi.Named(poiName, lookedUpPoiAddress);
  753. } else {
  754. poi = new Poi.Unnamed(lookedUpPoiAddress);
  755. }
  756. } catch (IOException e) {
  757. logger.error("Exception", e);
  758. //do not show this error!
  759. }
  760. messageModel.setLocationData(
  761. new LocationDataModel(
  762. location.getLatitude(),
  763. location.getLongitude(),
  764. (double) location.getAccuracy(),
  765. poi
  766. )
  767. );
  768. messageModel.setOutbox(true);
  769. messageModel.setState(MessageState.PENDING);
  770. messageModel.setSaved(true);
  771. receiver.saveLocalModel(messageModel);
  772. fireOnCreatedMessage(messageModel);
  773. receiver.createAndSendLocationMessage(messageModel);
  774. fireOnModifiedMessage(messageModel);
  775. if (completionHandler != null)
  776. completionHandler.sendQueued(messageModel);
  777. return messageModel;
  778. }
  779. @Override
  780. @WorkerThread
  781. public void resendMessage(
  782. @NonNull AbstractMessageModel messageModel,
  783. @NonNull MessageReceiver<AbstractMessageModel> receiver,
  784. @Nullable CompletionHandler completionHandler,
  785. @NonNull Collection<String> recipientIdentities,
  786. @NonNull MessageId messageId,
  787. @NonNull TriggerSource triggerSource
  788. ) throws Exception {
  789. NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
  790. notificationManager.cancel(NotificationIDs.UNSENT_MESSAGE_NOTIFICATION_ID);
  791. if (messageModel.getState() == MessageState.SENDFAILED || messageModel.getState() == MessageState.FS_KEY_MISMATCH) {
  792. if (messageModel.getType() == MessageType.FILE) {
  793. resendFileMessage(messageModel, receiver, completionHandler, recipientIdentities);
  794. } else if (messageModel.getType() == MessageType.BALLOT) {
  795. BallotModel ballotModel = ballotService.get(messageModel.getBallotData().getBallotId());
  796. if (ballotModel != null) {
  797. resendBallotMessage(messageModel, ballotModel, receiver, messageId, triggerSource);
  798. }
  799. } else if (messageModel.getType() == MessageType.TEXT) {
  800. resendTextMessage(messageModel, receiver, recipientIdentities);
  801. } else if (messageModel.getType() == MessageType.LOCATION) {
  802. resendLocationMessage(messageModel, receiver, completionHandler, recipientIdentities);
  803. }
  804. }
  805. }
  806. @WorkerThread
  807. private void resendTextMessage(
  808. final @NonNull AbstractMessageModel messageModel,
  809. final MessageReceiver receiver,
  810. final Collection<String> recipientIdentities
  811. ) {
  812. if (receiver instanceof ContactMessageReceiver && messageModel instanceof MessageModel) {
  813. ((ContactMessageReceiver) receiver).resendTextMessage((MessageModel) messageModel);
  814. } else if (receiver instanceof GroupMessageReceiver && messageModel instanceof GroupMessageModel) {
  815. ((GroupMessageReceiver) receiver).resendTextMessage(
  816. (GroupMessageModel) messageModel,
  817. recipientIdentities
  818. );
  819. } else if (receiver instanceof DistributionListMessageReceiver) {
  820. logger.warn("Cannot resend messages in a distribution list");
  821. return;
  822. } else {
  823. logger.warn("Incompatible message receiver and message model type");
  824. return;
  825. }
  826. updateOutgoingMessageState(messageModel, MessageState.SENDING, new Date());
  827. fireOnModifiedMessage(messageModel);
  828. }
  829. @WorkerThread
  830. private void resendLocationMessage(
  831. @NonNull AbstractMessageModel messageModel,
  832. @NonNull MessageReceiver receiver,
  833. final @Nullable CompletionHandler completionHandler,
  834. @NonNull Collection<String> recipientIdentities
  835. ) {
  836. if (receiver instanceof ContactMessageReceiver && messageModel instanceof MessageModel) {
  837. ((ContactMessageReceiver) receiver).resendLocationMessage((MessageModel) messageModel);
  838. } else if (receiver instanceof GroupMessageReceiver && messageModel instanceof GroupMessageModel) {
  839. ((GroupMessageReceiver) receiver).resendLocationMessage(
  840. (GroupMessageModel) messageModel,
  841. recipientIdentities
  842. );
  843. } else if (receiver instanceof DistributionListMessageReceiver) {
  844. logger.warn("Cannot resend messages in a distribution list");
  845. return;
  846. } else {
  847. logger.error("Incompatible message receiver and message model type");
  848. return;
  849. }
  850. updateOutgoingMessageState(messageModel, MessageState.SENDING, new Date());
  851. fireOnModifiedMessage(messageModel);
  852. if (completionHandler != null) {
  853. completionHandler.sendQueued(messageModel);
  854. }
  855. }
  856. @WorkerThread
  857. private void resendFileMessage(
  858. final @NonNull AbstractMessageModel messageModel,
  859. final @NonNull MessageReceiver<AbstractMessageModel> receiver,
  860. final @Nullable CompletionHandler completionHandler,
  861. final @NonNull Collection<String> recipientIdentities
  862. ) throws Exception {
  863. // check if a message file exists that could be resent or abort immediately
  864. File file = fileService.getMessageFile(messageModel);
  865. if (file == null || !file.exists()) {
  866. throw new ThreemaException("Message file not present");
  867. }
  868. updateOutgoingMessageState(messageModel, MessageState.PENDING, new Date());
  869. //enqueue processing and uploading stuff...
  870. messageSendingService.addToQueue(new MessageSendingService.MessageSendingProcess() {
  871. public byte[] blobIdThumbnail;
  872. public byte[] blobId;
  873. public byte[] thumbnailData;
  874. public byte[] fileData;
  875. public int fileDataBoxedLength;
  876. private SymmetricEncryptionResult contentEncryptResult;
  877. private SymmetricEncryptionResult thumbnailEncryptResult;
  878. public boolean success = false;
  879. @Override
  880. public MessageReceiver<AbstractMessageModel> getReceiver() {
  881. return receiver;
  882. }
  883. @Override
  884. public AbstractMessageModel getMessageModel() {
  885. return messageModel;
  886. }
  887. @Override
  888. public boolean send() throws Exception {
  889. SendMachine sendMachine = getSendMachine(messageModel);
  890. sendMachine.reset()
  891. .next(() -> {
  892. // get file data
  893. File decryptedMessageFile = fileService.getDecryptedMessageFile(messageModel);
  894. if (decryptedMessageFile != null) {
  895. try (FileInputStream inputStream = new FileInputStream(decryptedMessageFile)) {
  896. fileDataBoxedLength = inputStream.available();
  897. fileData = new byte[fileDataBoxedLength + NaCl.BOXOVERHEAD];
  898. IOUtils.readFully(inputStream,
  899. fileData,
  900. NaCl.BOXOVERHEAD,
  901. fileDataBoxedLength);
  902. }
  903. } else {
  904. throw new ThreemaException("Message file not present");
  905. }
  906. })
  907. .next(() -> {
  908. // encrypt file data
  909. contentEncryptResult = symmetricEncryptionService.encryptInplace(fileData, ProtocolDefines.FILE_NONCE);
  910. if (contentEncryptResult.isEmpty()) {
  911. throw new ThreemaException("File data encrypt failed");
  912. }
  913. })
  914. .next(() -> {
  915. // get thumbnail data
  916. try (InputStream is = fileService.getDecryptedMessageThumbnailStream(messageModel)) {
  917. if (is != null) {
  918. thumbnailData = IOUtils.toByteArray(is);
  919. } else {
  920. thumbnailData = null;
  921. }
  922. } catch (Exception e) {
  923. logger.debug("No thumbnail for file message");
  924. }
  925. })
  926. .next(() -> {
  927. // upload (encrypted) file data
  928. BlobUploader blobUploader = initUploader(
  929. getMessageModel(),
  930. contentEncryptResult.getData(),
  931. getReceiver()
  932. );
  933. blobUploader.progressListener = new ProgressListener() {
  934. @Override
  935. public void updateProgress(int progress) {
  936. updateMessageLoadingProgress(messageModel, progress);
  937. }
  938. @Override
  939. public void onFinished(boolean success) {
  940. setMessageLoadingFinished(messageModel);
  941. }
  942. };
  943. blobId = blobUploader.upload();
  944. })
  945. .next(() -> {
  946. if (thumbnailData != null) {
  947. // encrypt and upload thumbnail data
  948. thumbnailEncryptResult = symmetricEncryptionService.encrypt(thumbnailData, contentEncryptResult.getKey(), ProtocolDefines.FILE_THUMBNAIL_NONCE);
  949. if (thumbnailEncryptResult.isEmpty()) {
  950. throw new ThreemaException("Thumbnail encryption failed");
  951. } else {
  952. BlobUploader blobUploader = initUploader(
  953. getMessageModel(),
  954. thumbnailEncryptResult.getData(),
  955. getReceiver()
  956. );
  957. blobUploader.progressListener = new ProgressListener() {
  958. @Override
  959. public void updateProgress(int progress) {
  960. updateMessageLoadingProgress(messageModel, progress);
  961. }
  962. @Override
  963. public void onFinished(boolean success) {
  964. setMessageLoadingFinished(messageModel);
  965. }
  966. };
  967. blobIdThumbnail = blobUploader.upload();
  968. }
  969. }
  970. })
  971. .next(() -> {
  972. String messageId = messageModel.getApiMessageId();
  973. getReceiver().createAndSendFileMessage(
  974. blobIdThumbnail,
  975. blobId,
  976. contentEncryptResult,
  977. messageModel,
  978. messageId != null ? MessageId.fromString(messageId) : null,
  979. recipientIdentities
  980. );
  981. save(messageModel);
  982. })
  983. .next(() -> {
  984. updateOutgoingMessageState(messageModel, MessageState.SENDING, new Date());
  985. if (completionHandler != null)
  986. completionHandler.sendComplete(messageModel);
  987. success = true;
  988. });
  989. if (success) {
  990. removeSendMachine(sendMachine);
  991. }
  992. return success;
  993. }
  994. });
  995. }
  996. @Override
  997. public AbstractMessageModel sendBallotMessage(
  998. @NonNull BallotModel ballotModel,
  999. @NonNull MessageId messageId,
  1000. @NonNull TriggerSource triggerSource
  1001. ) throws MessageTooLongException {
  1002. //create a new ballot model
  1003. MessageReceiver receiver = ballotService.getReceiver(ballotModel);
  1004. if (receiver != null) {
  1005. //ok...
  1006. logger.debug("sendBallotMessage to {}", receiver);
  1007. final AbstractMessageModel messageModel = receiver.createLocalModel(MessageType.BALLOT, MessageContentsType.BALLOT, new Date());
  1008. cache(messageModel);
  1009. messageModel.setOutbox(true);
  1010. messageModel.setState(MessageState.PENDING);
  1011. messageModel.setBallotData(new BallotDataModel(
  1012. ballotModel.getState() == BallotModel.State.OPEN ?
  1013. BallotDataModel.Type.BALLOT_CREATED :
  1014. BallotDataModel.Type.BALLOT_CLOSED,
  1015. ballotModel.getId()));
  1016. messageModel.setSaved(true);
  1017. receiver.saveLocalModel(messageModel);
  1018. fireOnCreatedMessage(messageModel);
  1019. resendBallotMessage(messageModel, ballotModel, receiver, messageId, triggerSource);
  1020. return messageModel;
  1021. }
  1022. return null;
  1023. }
  1024. private void resendBallotMessage(
  1025. AbstractMessageModel messageModel,
  1026. BallotModel ballotModel,
  1027. MessageReceiver<?> receiver,
  1028. @NonNull MessageId messageId,
  1029. @NonNull TriggerSource triggerSource
  1030. ) throws MessageTooLongException {
  1031. //get ballot data
  1032. if (!TestUtil.required(messageModel, ballotModel, receiver)) {
  1033. return;
  1034. }
  1035. updateOutgoingMessageState(messageModel, MessageState.PENDING, new Date());
  1036. try {
  1037. ballotService.publish(receiver, ballotModel, messageModel, messageId, triggerSource);
  1038. } catch (NotAllowedException | MessageTooLongException x) {
  1039. logger.error("Exception", x);
  1040. if (x instanceof MessageTooLongException) {
  1041. remove(messageModel);
  1042. fireOnRemovedMessage(messageModel);
  1043. throw new MessageTooLongException();
  1044. } else {
  1045. updateOutgoingMessageState(messageModel, MessageState.SENDFAILED, new Date());
  1046. }
  1047. }
  1048. }
  1049. @Nullable
  1050. @Override
  1051. public MessageModel getContactMessageModel(
  1052. @NonNull final MessageId apiMessageId,
  1053. @NonNull final String identity
  1054. ) {
  1055. // Check contact message cache first
  1056. synchronized (contactMessageCache) {
  1057. MessageModel messageModel = Functional.select(contactMessageCache, m -> m.getApiMessageId() != null
  1058. && m.getApiMessageId().equals(apiMessageId.toString())
  1059. && TestUtil.compare(m.getIdentity(), identity));
  1060. if (messageModel != null) {
  1061. return messageModel;
  1062. }
  1063. }
  1064. // If not cached, load from database (and cache it)
  1065. MessageModel contactMessageModel = databaseService.getMessageModelFactory().getByApiMessageIdAndIdentity(
  1066. apiMessageId,
  1067. identity);
  1068. if (contactMessageModel != null) {
  1069. cache(contactMessageModel);
  1070. }
  1071. return contactMessageModel;
  1072. }
  1073. /**
  1074. * Get the AbstractMessageModel of a group message referenced by messageId, creatorId, and groupId
  1075. *
  1076. * @param messageId the message
  1077. * @param creatorIdentity the creator of the group
  1078. * @param groupId the group id
  1079. * @return a GroupMessageModel of the matching message or null in case a message could not be found
  1080. */
  1081. @Override
  1082. @Nullable
  1083. public GroupMessageModel getGroupMessageModel(
  1084. @NonNull final MessageId messageId,
  1085. @NonNull final String creatorIdentity,
  1086. @NonNull final GroupId groupId
  1087. ) {
  1088. String apiMessageIdString = messageId.toString();
  1089. if (apiMessageIdString == null) {
  1090. return null;
  1091. }
  1092. GroupModel groupModel = groupService.getByApiGroupIdAndCreator(groupId, creatorIdentity);
  1093. if (groupModel == null) {
  1094. return null;
  1095. }
  1096. // check group message cache first
  1097. synchronized (groupMessageCache) {
  1098. GroupMessageModel messageModel = Functional.select(groupMessageCache, m -> (apiMessageIdString.equals(m.getApiMessageId()) && groupModel.getId() == m.getGroupId()));
  1099. if (messageModel != null) {
  1100. return messageModel;
  1101. }
  1102. }
  1103. // retrieve from database
  1104. GroupMessageModel groupMessageModel = databaseService.getGroupMessageModelFactory().getByApiMessageIdAndGroupId(
  1105. messageId,
  1106. groupModel.getId());
  1107. if (groupMessageModel != null) {
  1108. cache(groupMessageModel);
  1109. return groupMessageModel;
  1110. }
  1111. return null;
  1112. }
  1113. @Override
  1114. public void updateOutgoingMessageState(
  1115. @NonNull AbstractMessageModel messageModel,
  1116. @NonNull MessageState state,
  1117. @NonNull Date date
  1118. ) {
  1119. if (!messageModel.isOutbox()) {
  1120. throw new IllegalArgumentException("Updating outgoing message state on incoming message " + messageModel.getApiMessageId());
  1121. }
  1122. if (MessageUtil.isReaction(state)) {
  1123. throw new IllegalArgumentException("The given message state is a reaction: " + state);
  1124. }
  1125. if (messageModel.isDeleted()) {
  1126. return;
  1127. }
  1128. synchronized (this) {
  1129. logger.debug(
  1130. "Updating message state from {} to {} at {}",
  1131. messageModel.getState(), state, date.getTime()
  1132. );
  1133. boolean hasChanges = true;
  1134. // Save date of state change
  1135. switch (state) {
  1136. case SENT:
  1137. // Note that we do not check whether the posted at time already exists as this
  1138. // value is already set when the message model has been created. We just update
  1139. // it when the message actually has been sent.
  1140. messageModel.setPostedAt(date);
  1141. messageModel.setModifiedAt(date);
  1142. break;
  1143. case DELIVERED:
  1144. if (messageModel.getDeliveredAt() != null) {
  1145. logger.warn("'Delivered at' already set for message {}", messageModel.getApiMessageId());
  1146. }
  1147. messageModel.setDeliveredAt(date);
  1148. messageModel.setModifiedAt(date);
  1149. break;
  1150. case READ:
  1151. if (messageModel.getReadAt() != null) {
  1152. logger.warn("'Read at' already set for message {}", messageModel.getApiMessageId());
  1153. }
  1154. messageModel.setReadAt(date);
  1155. messageModel.setModifiedAt(date);
  1156. break;
  1157. case SENDFAILED:
  1158. case FS_KEY_MISMATCH:
  1159. case CONSUMED:
  1160. messageModel.setModifiedAt(date);
  1161. break;
  1162. default:
  1163. hasChanges = false;
  1164. }
  1165. // Change the state only if it is possible
  1166. if (MessageUtil.canChangeToState(messageModel.getState(), state, messageModel instanceof GroupMessageModel)) {
  1167. messageModel.setState(state);
  1168. hasChanges = true;
  1169. } else {
  1170. logger.warn(
  1171. "State transition from {} to {}, ignoring",
  1172. messageModel.getState(), state
  1173. );
  1174. }
  1175. if (hasChanges) {
  1176. save(messageModel);
  1177. fireOnModifiedMessage(messageModel);
  1178. }
  1179. }
  1180. }
  1181. @Override
  1182. public void addMessageReaction(
  1183. @NonNull AbstractMessageModel messageModel,
  1184. @NonNull MessageState state,
  1185. @NonNull String fromIdentity,
  1186. @NonNull Date date
  1187. ) {
  1188. if (!MessageUtil.isReaction(state)) {
  1189. throw new IllegalArgumentException("The given message state is not a reaction: " + state);
  1190. }
  1191. updateAckDecState(messageModel, state, fromIdentity);
  1192. }
  1193. /**
  1194. * Special compatibility handling for state changes to ACK and DEC. Saves these messages to the reactions database
  1195. *
  1196. * @param messageModel The target message model of this state change / reaction
  1197. * @param newState The desired new state (ACK or DEC)
  1198. * @param senderIdentity The identity of the sender who sent this state change / reaction
  1199. */
  1200. private void updateAckDecState(@NonNull AbstractMessageModel messageModel, @NonNull MessageState newState, @Nullable String senderIdentity) {
  1201. if (newState != MessageState.USERACK && newState != MessageState.USERDEC) {
  1202. return;
  1203. }
  1204. if (senderIdentity == null) {
  1205. senderIdentity = identityStore.getIdentity();
  1206. }
  1207. clearMessageState(messageModel); // TODO(ANDR-3325): Remove
  1208. handleEmojiReaction(messageModel, newState, senderIdentity);
  1209. }
  1210. /**
  1211. * Map state changes (acknowledge and decline) to their emoji reaction equivalents keeping in account
  1212. * the mutual exclusivity of acks and decs
  1213. *
  1214. * @param messageModel The AbstractMessageModel of the target message
  1215. * @param state The desired new state
  1216. * @param fromIdentity The identity of the sender of this ack/dec reaction
  1217. */
  1218. private void handleEmojiReaction(AbstractMessageModel messageModel, MessageState state, String fromIdentity) {
  1219. if (state == MessageState.USERACK) {
  1220. saveEmojiReactionMessage(messageModel, fromIdentity, Reaction.ActionCase.WITHDRAW, EmojiUtil.THUMBS_DOWN_SEQUENCE);
  1221. saveEmojiReactionMessage(messageModel, fromIdentity, Reaction.ActionCase.APPLY, EmojiUtil.THUMBS_UP_SEQUENCE);
  1222. } else if (state == MessageState.USERDEC) {
  1223. saveEmojiReactionMessage(messageModel, fromIdentity, Reaction.ActionCase.WITHDRAW, EmojiUtil.THUMBS_UP_SEQUENCE);
  1224. saveEmojiReactionMessage(messageModel, fromIdentity, Reaction.ActionCase.APPLY, EmojiUtil.THUMBS_DOWN_SEQUENCE);
  1225. }
  1226. }
  1227. @Override
  1228. public boolean markAsRead(AbstractMessageModel message, boolean silent) throws ThreemaException {
  1229. logger.debug("markAsRead message = {} silent = {}", message.getApiMessageId(), silent);
  1230. boolean saved = false;
  1231. if (MessageUtil.canMarkAsRead(message)) {
  1232. ContactModel contactModel = contactService.getByIdentity(message.getIdentity());
  1233. // Check whether the message allows read receipt before setting the message to read
  1234. // because a message only allows a read receipt if has not been marked as read yet.
  1235. boolean messageAllowsDeliveryReceipt = MessageUtil.canSendDeliveryReceipt(message, ProtocolDefines.DELIVERYRECEIPT_MSGREAD);
  1236. Date readAt = new Date();
  1237. //save is read
  1238. message.setRead(true);
  1239. message.setReadAt(readAt);
  1240. message.setModifiedAt(readAt);
  1241. save(message);
  1242. if (!silent) {
  1243. //fire on modified if not silent
  1244. fireOnModifiedMessage(message);
  1245. }
  1246. saved = true;
  1247. if (contactModel == null) {
  1248. return saved;
  1249. }
  1250. if (message.getApiMessageId() == null) {
  1251. logger.info("Message id is null; cannot send read receipt or reflect message update");
  1252. return saved;
  1253. }
  1254. boolean receiverAllowsDeliveryReceipt;
  1255. switch (contactModel.getReadReceipts()) {
  1256. case ContactModel.SEND:
  1257. receiverAllowsDeliveryReceipt = true;
  1258. break;
  1259. case ContactModel.DONT_SEND:
  1260. receiverAllowsDeliveryReceipt = false;
  1261. break;
  1262. default:
  1263. receiverAllowsDeliveryReceipt = preferenceService.areReadReceiptsEnabled();
  1264. break;
  1265. }
  1266. if (messageAllowsDeliveryReceipt && receiverAllowsDeliveryReceipt) {
  1267. contactService.createReceiver(contactModel).sendDeliveryReceipt(
  1268. ProtocolDefines.DELIVERYRECEIPT_MSGREAD,
  1269. new MessageId[]{MessageId.fromString(message.getApiMessageId())},
  1270. readAt.getTime()
  1271. );
  1272. logger.info("Enqueued delivery receipt (read) message for message ID {} from {}",
  1273. message.getApiMessageId(), contactModel.getIdentity());
  1274. } else {
  1275. if (message instanceof MessageModel) {
  1276. contactService.createReceiver(contactModel).sendIncomingMessageUpdateRead(
  1277. Set.of(MessageId.fromString(message.getApiMessageId())), readAt.getTime()
  1278. );
  1279. } else if (message instanceof GroupMessageModel) {
  1280. int localGroupId = ((GroupMessageModel) message).getGroupId();
  1281. GroupModel groupModel = groupService.getById(localGroupId);
  1282. if (groupModel != null) {
  1283. groupService.createReceiver(groupModel).sendIncomingMessageUpdateRead(
  1284. Set.of(MessageId.fromString(message.getApiMessageId())),
  1285. readAt.getTime()
  1286. );
  1287. } else {
  1288. logger.warn("Could not find group with local group id {}", localGroupId);
  1289. }
  1290. }
  1291. }
  1292. }
  1293. return saved;
  1294. }
  1295. @Override
  1296. @WorkerThread
  1297. public boolean markAsConsumed(AbstractMessageModel message) throws ThreemaException {
  1298. logger.debug("markAsConsumed message = {}", message.getApiMessageId());
  1299. boolean saved = false;
  1300. if (MessageUtil.canMarkAsConsumed(message)) {
  1301. // save consumed state
  1302. message.setState(MessageState.CONSUMED);
  1303. message.setModifiedAt(new Date());
  1304. save(message);
  1305. saved = true;
  1306. fireOnModifiedMessage(message);
  1307. }
  1308. return saved;
  1309. }
  1310. @Override
  1311. public void remove(AbstractMessageModel messageModel) {
  1312. remove(messageModel, false);
  1313. }
  1314. @Override
  1315. public void remove(final AbstractMessageModel messageModel, boolean silent) {
  1316. SendMachine machine = getSendMachine(messageModel, false);
  1317. if (machine != null) {
  1318. //abort pending send machine
  1319. //do not remove SendMachine (fix ANDR-522)
  1320. machine.abort();
  1321. }
  1322. //remove pending uploads
  1323. cancelUploader(messageModel);
  1324. //remove from sdcard
  1325. fileService.removeMessageFiles(messageModel, true);
  1326. //remove from dao
  1327. if (messageModel instanceof GroupMessageModel) {
  1328. databaseService.getGroupMessageModelFactory().delete(
  1329. (GroupMessageModel) messageModel
  1330. );
  1331. //remove from cache
  1332. synchronized (groupMessageCache) {
  1333. Iterator<GroupMessageModel> i = groupMessageCache.iterator();
  1334. while (i.hasNext()) {
  1335. if (i.next().getId() == messageModel.getId()) {
  1336. i.remove();
  1337. }
  1338. }
  1339. }
  1340. } else if (messageModel instanceof DistributionListMessageModel) {
  1341. databaseService.getDistributionListMessageModelFactory().delete(
  1342. (DistributionListMessageModel) messageModel
  1343. );
  1344. //remove from cache
  1345. synchronized (distributionListMessageCache) {
  1346. Iterator<DistributionListMessageModel> i = distributionListMessageCache.iterator();
  1347. while (i.hasNext()) {
  1348. if (i.next().getId() == messageModel.getId()) {
  1349. i.remove();
  1350. }
  1351. }
  1352. }
  1353. } else if (messageModel instanceof MessageModel) {
  1354. databaseService.getMessageModelFactory().delete((MessageModel) messageModel);
  1355. //remove from cache
  1356. synchronized (contactMessageCache) {
  1357. Iterator<MessageModel> i = contactMessageCache.iterator();
  1358. while (i.hasNext()) {
  1359. if (i.next().getId() == messageModel.getId()) {
  1360. i.remove();
  1361. }
  1362. }
  1363. }
  1364. }
  1365. if (!silent) {
  1366. fireOnRemovedMessage(messageModel);
  1367. }
  1368. }
  1369. @Override
  1370. public boolean processIncomingContactMessage(final AbstractMessage message, @NonNull TriggerSource triggerSource) throws Exception {
  1371. logger.info("processIncomingContactMessage: {}", message.getMessageId());
  1372. final String senderIdentity = message.getFromIdentity();
  1373. if (senderIdentity == null) {
  1374. logger.error("Could not process a message of type {} without a sender identity", message.getType());
  1375. return false;
  1376. }
  1377. MessageModel messageModel = null;
  1378. MessageModel existingModel = databaseService.getMessageModelFactory()
  1379. .getByApiMessageIdAndIdentity(message.getMessageId(), message.getFromIdentity());
  1380. if (existingModel != null) {
  1381. //first search in cache
  1382. MessageModel savedMessageModel;
  1383. logger.info("processIncomingContactMessage: {} check contact message cache", message.getMessageId());
  1384. synchronized (contactMessageCache) {
  1385. savedMessageModel = Functional.select(contactMessageCache, messageModel1 -> messageModel1.getApiMessageId() != null &&
  1386. messageModel1.getApiMessageId().equals(message.getMessageId().toString())
  1387. && senderIdentity.equals(messageModel1.getIdentity()));
  1388. }
  1389. logger.info("processIncomingContactMessage: {} check contact message cache end", message.getMessageId());
  1390. if (savedMessageModel == null) {
  1391. //get from sql result
  1392. savedMessageModel = existingModel;
  1393. }
  1394. if (savedMessageModel.isSaved()) {
  1395. //do nothing!
  1396. return true;
  1397. } else {
  1398. messageModel = savedMessageModel;
  1399. }
  1400. }
  1401. // Look up contact model
  1402. //
  1403. // Note: At this point, the contact should have been created by the IncomingMessageProcessor.
  1404. final ContactModel contactModel = contactService.getByIdentity(senderIdentity);
  1405. if (contactModel == null) {
  1406. logger.error("Could not process a message of type {} from an unknown contact", message.getType());
  1407. logger.info("processIncomingContactMessage: {} FAILED", message.getMessageId());
  1408. return false;
  1409. }
  1410. // As soon as we get a direct message, unhide and unarchive the contact
  1411. contactService.setAcquaintanceLevel(senderIdentity, ContactModel.AcquaintanceLevel.DIRECT);
  1412. contactService.setIsArchived(senderIdentity, false, triggerSource);
  1413. // Bump "lastUpdate" if necessary, depending on the message type. Note that due to the
  1414. // listeners, we should bump the last update before saving the box message. Saving the box
  1415. // message will trigger the listeners that, among other things, update the webclient. For
  1416. // this purpose it is important that the last update flag has already been bumped.
  1417. if (message.bumpLastUpdate()) {
  1418. contactService.bumpLastUpdate(senderIdentity);
  1419. }
  1420. // Handle message depending on subtype
  1421. final Class<? extends AbstractMessage> messageClass = message.getClass();
  1422. if (messageClass.equals(TextMessage.class)) {
  1423. messageModel = saveBoxMessage((TextMessage) message, messageModel, contactModel);
  1424. } else if (messageClass.equals(ImageMessage.class)) {
  1425. messageModel = saveBoxMessage((ImageMessage) message, messageModel, contactModel);
  1426. // silently save to gallery if enabled
  1427. if (
  1428. preferenceService != null
  1429. && preferenceService.isSaveMedia()
  1430. && messageModel.getImageData().isDownloaded()
  1431. && !conversationCategoryService.isPrivateChat(ContactUtil.getUniqueIdString(messageModel.getIdentity()))
  1432. ) {
  1433. fileService.saveMedia(null, null, new CopyOnWriteArrayList<>(Collections.singletonList(messageModel)), true);
  1434. }
  1435. } else if (messageClass.equals(VideoMessage.class)) {
  1436. messageModel = saveBoxMessage((VideoMessage) message, messageModel, contactModel);
  1437. } else if (messageClass.equals(LocationMessage.class)) {
  1438. messageModel = saveBoxMessage((LocationMessage) message, messageModel, contactModel);
  1439. } else if (messageClass.equals(AudioMessage.class)) {
  1440. messageModel = saveBoxMessage((AudioMessage) message, messageModel, contactModel);
  1441. } else if (messageClass.equals(PollSetupMessage.class)) {
  1442. messageModel = saveBoxMessage((PollSetupMessage) message, messageModel, contactModel);
  1443. }
  1444. if (messageModel == null) {
  1445. logger.info("processIncomingContactMessage: {} FAILED", message.getMessageId());
  1446. return false;
  1447. }
  1448. logger.info("processIncomingContactMessage: {} SUCCESS - Message ID = {}", message.getMessageId(), messageModel.getId());
  1449. return true;
  1450. }
  1451. @Override
  1452. public boolean processIncomingGroupMessage(
  1453. @NonNull AbstractGroupMessage message,
  1454. @NonNull TriggerSource triggerSource
  1455. ) throws Exception {
  1456. logger.info("processIncomingGroupMessage: {}", message.getMessageId());
  1457. GroupMessageModel messageModel = null;
  1458. // First of all, check if i can receive messages. Note that the common group receive steps
  1459. // must have been executed at this point.
  1460. GroupModel groupModel = groupService.getByGroupMessage(message);
  1461. if (groupModel == null) {
  1462. logger.error("GroupMessage {}: error: no groupModel", message.getMessageId());
  1463. return false;
  1464. }
  1465. //is allowed?
  1466. GroupAccessModel access = groupService.getAccess(groupModel, false);
  1467. if (access == null ||
  1468. !access.getCanReceiveMessageAccess().isAllowed()) {
  1469. //not allowed to receive a message, ignore message but
  1470. //set success to true (remove from server)
  1471. logger.error("GroupMessage {}: error: not allowed", message.getMessageId());
  1472. return true;
  1473. }
  1474. // is the user blocked?
  1475. if (blockedIdentitiesService.isBlocked(message.getFromIdentity())) {
  1476. //set success to true (remove from server)
  1477. logger.info("GroupMessage {}: Sender is blocked, ignoring", message.getMessageId());
  1478. return true;
  1479. }
  1480. // reset archived status
  1481. groupService.setIsArchived(
  1482. groupModel.getCreatorIdentity(),
  1483. groupModel.getApiGroupId(),
  1484. false,
  1485. triggerSource
  1486. );
  1487. // Bump "lastUpdate" if necessary, depending on the message type
  1488. //
  1489. // Note: Do this before the message is saved! Saving the message will trigger listeners
  1490. // that will re-sort the conversation list. At that point in time, lastUpdate should already
  1491. // be correct.
  1492. if (message.bumpLastUpdate()) {
  1493. groupService.bumpLastUpdate(groupModel);
  1494. }
  1495. GroupMessageModel existingModel = databaseService.getGroupMessageModelFactory().getByApiMessageIdAndIdentity(
  1496. message.getMessageId(),
  1497. message.getFromIdentity()
  1498. );
  1499. if (existingModel != null) {
  1500. if (existingModel.isSaved()) {
  1501. //do nothing!
  1502. logger.error("GroupMessage {}: error: message already exists", message.getMessageId());
  1503. return true;
  1504. } else {
  1505. //use the first non saved model to edit!
  1506. logger.error("GroupMessage {}: error: reusing unsaved model", message.getMessageId());
  1507. messageModel = existingModel;
  1508. }
  1509. }
  1510. if (message.getClass().equals(GroupTextMessage.class)) {
  1511. messageModel = saveGroupMessage((GroupTextMessage) message, messageModel);
  1512. } else if (message.getClass().equals(GroupImageMessage.class)) {
  1513. messageModel = saveGroupMessage((GroupImageMessage) message, messageModel);
  1514. // silently save to gallery if enabled
  1515. if (messageModel != null
  1516. && preferenceService != null
  1517. && preferenceService.isSaveMedia()
  1518. && messageModel.getImageData().isDownloaded()
  1519. && !conversationCategoryService.isPrivateChat(GroupUtil.getUniqueIdString(groupModel))) {
  1520. fileService.saveMedia(null, null, new CopyOnWriteArrayList<>(Collections.singletonList(messageModel)), true);
  1521. }
  1522. } else if (message.getClass().equals(GroupVideoMessage.class)) {
  1523. messageModel = saveGroupMessage((GroupVideoMessage) message, messageModel);
  1524. } else if (message.getClass().equals(GroupLocationMessage.class)) {
  1525. messageModel = saveGroupMessage((GroupLocationMessage) message, messageModel);
  1526. } else if (message.getClass().equals(GroupAudioMessage.class)) {
  1527. messageModel = saveGroupMessage((GroupAudioMessage) message, messageModel);
  1528. } else if (message.getClass().equals(GroupPollSetupMessage.class)) {
  1529. messageModel = saveGroupMessage((GroupPollSetupMessage) message, messageModel);
  1530. // This is only used for debugging
  1531. if (ConfigUtils.isDevBuild()) {
  1532. logger.info("Processed GroupBallotCreateMessage {}", ((GroupPollSetupMessage) message).rawBallotData);
  1533. }
  1534. }
  1535. if (messageModel != null) {
  1536. logger.info("processIncomingGroupMessage: {} SUCCESS - Message ID = {}", message.getMessageId(), messageModel.getId());
  1537. return true;
  1538. } else {
  1539. logger.info("processIncomingGroupMessage: {} FAILED", message.getMessageId());
  1540. return false;
  1541. }
  1542. }
  1543. /**
  1544. * Process a 1:1 text message (0x01).
  1545. */
  1546. private MessageModel saveBoxMessage(
  1547. @NonNull TextMessage message,
  1548. MessageModel messageModel,
  1549. @NonNull ContactModel contactModel
  1550. ) {
  1551. if (messageModel == null) {
  1552. ContactMessageReceiver r = contactService.createReceiver(contactModel);
  1553. messageModel = r.createLocalModel(MessageType.TEXT, MessageContentsType.TEXT, message.getDate());
  1554. cache(messageModel);
  1555. messageModel.setApiMessageId(message.getMessageId().toString());
  1556. messageModel.setMessageFlags(message.getMessageFlags());
  1557. messageModel.setOutbox(false);
  1558. // replace CR by LF for Window$ Phone compatibility - me be removed soon.
  1559. String body = message.getText() != null ? message.getText().replace("\r", "\n") : null;
  1560. messageModel.setBodyAndQuotedMessageId(body);
  1561. messageModel.setIdentity(contactModel.getIdentity());
  1562. messageModel.setForwardSecurityMode(message.getForwardSecurityMode());
  1563. messageModel.setSaved(true);
  1564. databaseService.getMessageModelFactory().create(messageModel);
  1565. fireOnNewMessage(messageModel);
  1566. }
  1567. return messageModel;
  1568. }
  1569. /**
  1570. * Process a 1:1 poll setup message (0x15).
  1571. */
  1572. private MessageModel saveBoxMessage(
  1573. @NonNull PollSetupMessage message,
  1574. MessageModel messageModel,
  1575. @NonNull ContactModel contactModel
  1576. ) throws Exception {
  1577. MessageReceiver messageReceiver = contactService.createReceiver(contactModel);
  1578. return (MessageModel) saveBallotCreateMessage(
  1579. messageReceiver,
  1580. message.getMessageId(),
  1581. message,
  1582. messageModel,
  1583. message.getMessageFlags(),
  1584. message.getForwardSecurityMode(),
  1585. // Note that this may also be remote, but it is certainly never local. To be safe,
  1586. // we use sync as this will prevent sending any csp messages.
  1587. TriggerSource.SYNC
  1588. );
  1589. }
  1590. private GroupMessageModel saveGroupMessage(GroupPollSetupMessage message, GroupMessageModel messageModel) throws Exception {
  1591. GroupModel groupModel = groupService.getByGroupMessage(message);
  1592. if (groupModel == null) {
  1593. return null;
  1594. }
  1595. MessageReceiver messageReceiver = groupService.createReceiver(groupModel);
  1596. return (GroupMessageModel) saveBallotCreateMessage(
  1597. messageReceiver,
  1598. message.getMessageId(),
  1599. message,
  1600. messageModel,
  1601. message.getMessageFlags(),
  1602. message.getForwardSecurityMode(),
  1603. // Note that this may also be remote, but it is certainly never local. To be safe,
  1604. // we use sync as this will prevent sending any csp messages.
  1605. TriggerSource.SYNC
  1606. );
  1607. }
  1608. @Override
  1609. public AbstractMessageModel saveBallotCreateMessage(
  1610. @NonNull MessageReceiver<?> receiver,
  1611. @NonNull MessageId messageId,
  1612. @NonNull BallotSetupInterface message,
  1613. @Nullable AbstractMessageModel messageModel,
  1614. int messageFlags,
  1615. @Nullable ForwardSecurityMode forwardSecurityMode,
  1616. @NonNull TriggerSource triggerSource
  1617. ) throws ThreemaException, BadMessageException {
  1618. BallotUpdateResult result = ballotService.update(message, messageId, triggerSource);
  1619. if (result.getBallotModel() == null) {
  1620. throw new ThreemaException("could not create ballot model");
  1621. }
  1622. switch (result.getOperation()) {
  1623. case CREATE:
  1624. case CLOSE:
  1625. messageModel = createNewBallotMessage(
  1626. messageId,
  1627. result.getBallotModel(),
  1628. (result.getOperation() == BallotUpdateResult.Operation.CREATE ?
  1629. BallotDataModel.Type.BALLOT_CREATED :
  1630. BallotDataModel.Type.BALLOT_CLOSED),
  1631. receiver,
  1632. messageFlags,
  1633. forwardSecurityMode);
  1634. }
  1635. return messageModel;
  1636. }
  1637. @Deprecated
  1638. private AbstractMessageModel saveAudioMessage(@NonNull MessageReceiver receiver,
  1639. AbstractMessage message,
  1640. AbstractMessageModel messageModel) {
  1641. boolean newModel = false;
  1642. int duration;
  1643. byte[] encryptionKey, audioBlobId;
  1644. if (message instanceof GroupAudioMessage) {
  1645. duration = ((GroupAudioMessage) message).getDuration();
  1646. encryptionKey = ((GroupAudioMessage) message).getEncryptionKey();
  1647. audioBlobId = ((GroupAudioMessage) message).getAudioBlobId();
  1648. } else if (message instanceof AudioMessage) {
  1649. duration = ((AudioMessage) message).durationInSeconds;
  1650. encryptionKey = ((AudioMessage) message).encryptionKey;
  1651. audioBlobId = ((AudioMessage) message).audioBlobId;
  1652. } else {
  1653. return null;
  1654. }
  1655. if (messageModel == null) {
  1656. newModel = true;
  1657. messageModel = receiver.createLocalModel(MessageType.VOICEMESSAGE, MessageContentsType.VOICE_MESSAGE, message.getDate());
  1658. cache(messageModel);
  1659. messageModel.setApiMessageId(message.getMessageId().toString());
  1660. messageModel.setMessageFlags(message.getMessageFlags());
  1661. messageModel.setOutbox(false);
  1662. messageModel.setIdentity(message.getFromIdentity());
  1663. messageModel.setAudioData(new AudioDataModel(duration, audioBlobId, encryptionKey));
  1664. messageModel.setForwardSecurityMode(message.getForwardSecurityMode());
  1665. //create the record
  1666. receiver.saveLocalModel(messageModel);
  1667. }
  1668. messageModel.setSaved(true);
  1669. receiver.saveLocalModel(messageModel);
  1670. if (newModel) {
  1671. fireOnCreatedMessage(messageModel);
  1672. if (shouldAutoDownload(MessageType.VOICEMESSAGE)) {
  1673. try {
  1674. downloadMediaMessage(messageModel, null);
  1675. } catch (Exception e) {
  1676. // a failed blob auto-download should not be considered a failure as the user can try again manually
  1677. logger.error("Unable to auto-download blob", e);
  1678. }
  1679. }
  1680. } else {
  1681. fireOnModifiedMessage(messageModel);
  1682. }
  1683. return messageModel;
  1684. }
  1685. @Deprecated
  1686. private AbstractMessageModel saveVideoMessage(
  1687. @NonNull MessageReceiver receiver,
  1688. AbstractMessage message,
  1689. AbstractMessageModel messageModel
  1690. ) throws Exception {
  1691. boolean newModel = false;
  1692. int duration, videoSize;
  1693. byte[] encryptionKey, videoBlobId, thumbnailBlobId;
  1694. if (message instanceof GroupVideoMessage) {
  1695. duration = ((GroupVideoMessage) message).getDuration();
  1696. videoSize = ((GroupVideoMessage) message).getVideoSize();
  1697. encryptionKey = ((GroupVideoMessage) message).getEncryptionKey();
  1698. videoBlobId = ((GroupVideoMessage) message).getVideoBlobId();
  1699. thumbnailBlobId = ((GroupVideoMessage) message).getThumbnailBlobId();
  1700. } else if (message instanceof VideoMessage) {
  1701. duration = ((VideoMessage) message).durationInSeconds;
  1702. videoSize = ((VideoMessage) message).videoSizeInBytes;
  1703. encryptionKey = ((VideoMessage) message).encryptionKey;
  1704. videoBlobId = ((VideoMessage) message).videoBlobId;
  1705. thumbnailBlobId = ((VideoMessage) message).thumbnailBlobId;
  1706. } else {
  1707. return null;
  1708. }
  1709. if (messageModel == null) {
  1710. newModel = true;
  1711. messageModel = receiver.createLocalModel(MessageType.VIDEO, MessageContentsType.VIDEO, message.getDate());
  1712. cache(messageModel);
  1713. messageModel.setApiMessageId(message.getMessageId().toString());
  1714. messageModel.setMessageFlags(message.getMessageFlags());
  1715. messageModel.setOutbox(false);
  1716. messageModel.setIdentity(message.getFromIdentity());
  1717. messageModel.setVideoData(new VideoDataModel(duration, videoSize, videoBlobId, encryptionKey));
  1718. messageModel.setForwardSecurityMode(message.getForwardSecurityMode());
  1719. //create the record
  1720. receiver.saveLocalModel(messageModel);
  1721. }
  1722. //download thumbnail
  1723. final AbstractMessageModel messageModel1 = messageModel;
  1724. //use download service!
  1725. logger.info("Downloading blob for message {} id = {}", messageModel.getApiMessageId(), messageModel.getId());
  1726. // If multi-device is active, we always mark as done. Otherwise we do not mark as done if its a group message
  1727. boolean shouldMarkAsDone = multiDeviceManager.isMultiDeviceActive() || !(message instanceof AbstractGroupMessage);
  1728. @Nullable BlobScope blobScopeMarkAsDone = null;
  1729. if (shouldMarkAsDone) {
  1730. blobScopeMarkAsDone = messageModel.getBlobScopeForMarkAsDone();
  1731. }
  1732. byte[] thumbnailBlob = downloadService.download(
  1733. messageModel.getId(),
  1734. thumbnailBlobId,
  1735. messageModel.getBlobScopeForDownload(),
  1736. blobScopeMarkAsDone,
  1737. new ProgressListener() {
  1738. @Override
  1739. public void updateProgress(int progress) {
  1740. updateMessageLoadingProgress(messageModel1, progress);
  1741. }
  1742. @Override
  1743. public void onFinished(boolean success) {
  1744. setMessageLoadingFinished(messageModel1);
  1745. }
  1746. }
  1747. );
  1748. if (thumbnailBlob != null && thumbnailBlob.length > NaCl.BOXOVERHEAD) {
  1749. byte[] thumbnail = symmetricEncryptionService.decrypt(thumbnailBlob, encryptionKey, ProtocolDefines.THUMBNAIL_NONCE);
  1750. if (thumbnail != null) {
  1751. try {
  1752. fileService.writeConversationMediaThumbnail(messageModel, thumbnail);
  1753. } catch (Exception e) {
  1754. downloadService.error(messageModel.getId());
  1755. throw e;
  1756. }
  1757. }
  1758. messageModel.setSaved(true);
  1759. receiver.saveLocalModel(messageModel);
  1760. downloadService.complete(messageModel.getId(), thumbnailBlobId);
  1761. if (newModel) {
  1762. fireOnCreatedMessage(messageModel);
  1763. if (shouldAutoDownload(MessageType.VIDEO)) {
  1764. if (videoSize <= FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO) {
  1765. try {
  1766. downloadMediaMessage(messageModel, null);
  1767. } catch (Exception e) {
  1768. // a failed blob auto-download should not be considered a failure as the user can try again manually
  1769. logger.error("Unable to auto-download blob", e);
  1770. }
  1771. }
  1772. }
  1773. } else {
  1774. fireOnModifiedMessage(messageModel);
  1775. }
  1776. return messageModel;
  1777. }
  1778. downloadService.error(messageModel.getId());
  1779. return null;
  1780. }
  1781. /**
  1782. * @return {@code true} if the thumbnail was downloaded and saved
  1783. */
  1784. @Override
  1785. public boolean downloadThumbnailIfPresent(@NonNull FileData fileData, @NonNull AbstractMessageModel messageModel) throws Exception {
  1786. if (fileData.getThumbnailBlobId() == null) {
  1787. return false;
  1788. }
  1789. logger.info("Downloading thumbnail of message {}", messageModel.getApiMessageId());
  1790. final AbstractMessageModel messageModel1 = messageModel;
  1791. // If multi-device is active, we always mark as done. Otherwise we do not mark as done if its a group message
  1792. boolean shouldMarkAsDone = multiDeviceManager.isMultiDeviceActive() || !(messageModel instanceof GroupMessageModel);
  1793. @Nullable BlobScope blobScopeMarkAsDone = null;
  1794. if (shouldMarkAsDone) {
  1795. blobScopeMarkAsDone = messageModel.getBlobScopeForMarkAsDone();
  1796. }
  1797. byte[] thumbnailBlob = downloadService.download(
  1798. messageModel.getId(),
  1799. fileData.getThumbnailBlobId(),
  1800. messageModel.getBlobScopeForDownload(),
  1801. blobScopeMarkAsDone,
  1802. new ProgressListener() {
  1803. @Override
  1804. public void updateProgress(int progress) {
  1805. updateMessageLoadingProgress(messageModel1, progress);
  1806. }
  1807. @Override
  1808. public void onFinished(boolean success) {
  1809. setMessageLoadingFinished(messageModel1);
  1810. }
  1811. });
  1812. if (thumbnailBlob == null) {
  1813. downloadService.error(messageModel.getId());
  1814. logger.info("Error downloading thumbnail for message {}", messageModel.getApiMessageId());
  1815. throw new ThreemaException("Error downloading thumbnail");
  1816. }
  1817. byte[] thumbnail = symmetricEncryptionService.decrypt(thumbnailBlob, fileData.getEncryptionKey(), ProtocolDefines.FILE_THUMBNAIL_NONCE);
  1818. if (thumbnail != null) {
  1819. try {
  1820. fileService.writeConversationMediaThumbnail(messageModel, thumbnail);
  1821. } catch (Exception exception) {
  1822. downloadService.error(messageModel.getId());
  1823. logger.info("Error writing thumbnail for message {}", messageModel.getApiMessageId());
  1824. throw exception;
  1825. }
  1826. }
  1827. downloadService.complete(messageModel.getId(), fileData.getThumbnailBlobId());
  1828. return true;
  1829. }
  1830. private GroupMessageModel saveGroupMessage(GroupTextMessage message, GroupMessageModel messageModel) {
  1831. GroupModel groupModel = groupService.getByGroupMessage(message);
  1832. if (groupModel == null) {
  1833. return null;
  1834. }
  1835. if (messageModel == null) {
  1836. GroupMessageReceiver r = groupService.createReceiver(groupModel);
  1837. messageModel = r.createLocalModel(MessageType.TEXT, MessageContentsType.TEXT, message.getDate());
  1838. cache(messageModel);
  1839. messageModel.setApiMessageId(message.getMessageId().toString());
  1840. messageModel.setMessageFlags(message.getMessageFlags());
  1841. messageModel.setOutbox(false);
  1842. // replace CR by LF for Window$ Phone compatibility - me be removed soon.
  1843. String body = message.getText() != null ? message.getText().replace("\r", "\n") : null;
  1844. messageModel.setBodyAndQuotedMessageId(body);
  1845. messageModel.setSaved(true);
  1846. messageModel.setIdentity(message.getFromIdentity());
  1847. messageModel.setForwardSecurityMode(message.getForwardSecurityMode());
  1848. r.saveLocalModel(messageModel);
  1849. fireOnNewMessage(messageModel);
  1850. }
  1851. return messageModel;
  1852. }
  1853. private boolean shouldAutoDownload(MessageType type) {
  1854. if (preferenceService != null) {
  1855. ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1856. NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
  1857. if (activeNetwork != null) {
  1858. switch (activeNetwork.getType()) {
  1859. case ConnectivityManager.TYPE_ETHERNET:
  1860. // fallthrough
  1861. case ConnectivityManager.TYPE_WIFI:
  1862. return preferenceService.getWifiAutoDownload().contains(String.valueOf(type.ordinal()));
  1863. case ConnectivityManager.TYPE_MOBILE:
  1864. return preferenceService.getMobileAutoDownload().contains(String.valueOf(type.ordinal()));
  1865. default:
  1866. break;
  1867. }
  1868. }
  1869. }
  1870. return false;
  1871. }
  1872. /**
  1873. * Check if the file in question should be auto-downloaded or not
  1874. * This depends on file type, file size and user preference (settings)
  1875. *
  1876. * @param messageModel AbstractMessageModel to check
  1877. * @return true if file should be downloaded immediately, false otherwise
  1878. */
  1879. @Override
  1880. public boolean shouldAutoDownload(@NonNull AbstractMessageModel messageModel) {
  1881. MessageType type = MessageType.FILE;
  1882. FileDataModel fileDataModel = messageModel.getFileData();
  1883. if (fileDataModel.getRenderingType() != FileData.RENDERING_DEFAULT) {
  1884. // treat media with default (file) rendering like a file for the sake of auto-download
  1885. if (messageModel.getMessageContentsType() == MessageContentsType.IMAGE) {
  1886. type = MessageType.IMAGE;
  1887. } else if (messageModel.getMessageContentsType() == MessageContentsType.VIDEO) {
  1888. type = MessageType.VIDEO;
  1889. } else if (messageModel.getMessageContentsType() == MessageContentsType.VOICE_MESSAGE) {
  1890. type = MessageType.VOICEMESSAGE;
  1891. }
  1892. }
  1893. if (preferenceService != null) {
  1894. ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1895. NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
  1896. if (activeNetwork != null) {
  1897. boolean canDownload = false;
  1898. switch (activeNetwork.getType()) {
  1899. case ConnectivityManager.TYPE_ETHERNET:
  1900. // fallthrough
  1901. case ConnectivityManager.TYPE_WIFI:
  1902. canDownload = preferenceService.getWifiAutoDownload().contains(String.valueOf(type.ordinal()));
  1903. break;
  1904. case ConnectivityManager.TYPE_MOBILE:
  1905. canDownload = preferenceService.getMobileAutoDownload().contains(String.valueOf(type.ordinal()));
  1906. break;
  1907. default:
  1908. break;
  1909. }
  1910. if (canDownload) {
  1911. // images and voice messages are always auto-downloaded regardless of size
  1912. return
  1913. type == MessageType.IMAGE ||
  1914. type == MessageType.VOICEMESSAGE ||
  1915. fileDataModel.getFileSize() <= FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO;
  1916. }
  1917. }
  1918. }
  1919. return false;
  1920. }
  1921. @Deprecated
  1922. private GroupMessageModel saveGroupMessage(GroupImageMessage message, GroupMessageModel messageModel) {
  1923. GroupModel groupModel = groupService.getByGroupMessage(message);
  1924. if (groupModel == null) {
  1925. return null;
  1926. }
  1927. GroupMessageModelFactory messageModelFactory = databaseService.getGroupMessageModelFactory();
  1928. //download thumbnail
  1929. if (messageModel == null) {
  1930. MessageReceiver r = groupService.createReceiver(groupModel);
  1931. messageModel = (GroupMessageModel) r.createLocalModel(MessageType.IMAGE, MessageContentsType.IMAGE, message.getDate());
  1932. cache(messageModel);
  1933. messageModel.setApiMessageId(message.getMessageId().toString());
  1934. messageModel.setMessageFlags(message.getMessageFlags());
  1935. messageModel.setOutbox(false);
  1936. messageModel.setIdentity(message.getFromIdentity());
  1937. messageModel.setImageData(new ImageDataModel(
  1938. message.getBlobId(),
  1939. message.getEncryptionKey(),
  1940. ProtocolDefines.IMAGE_NONCE
  1941. ));
  1942. // Mark as saved to show message without image e.g.
  1943. messageModel.setSaved(true);
  1944. r.saveLocalModel(messageModel);
  1945. }
  1946. fireOnNewMessage(messageModel);
  1947. final GroupMessageModel messageModel1 = messageModel;
  1948. if (shouldAutoDownload(MessageType.IMAGE) && !messageModel.getImageData().isDownloaded()) {
  1949. // If multi-device is active, we always mark as done (even for a group message)
  1950. boolean shouldMarkAsDone = multiDeviceManager.isMultiDeviceActive();
  1951. @Nullable BlobScope blobScopeMarkAsDone = null;
  1952. if (shouldMarkAsDone) {
  1953. blobScopeMarkAsDone = messageModel.getBlobScopeForMarkAsDone();
  1954. }
  1955. byte[] blob = downloadService.download(
  1956. messageModel.getId(),
  1957. message.getBlobId(),
  1958. messageModel.getBlobScopeForDownload(),
  1959. blobScopeMarkAsDone,
  1960. new ProgressListener() {
  1961. // do we really need a progress listener for images?
  1962. @Override
  1963. public void updateProgress(int progress) {
  1964. updateMessageLoadingProgress(messageModel1, progress);
  1965. }
  1966. @Override
  1967. public void onFinished(boolean success) {
  1968. setMessageLoadingFinished(messageModel1);
  1969. }
  1970. }
  1971. );
  1972. if (blob != null && messageModel.getImageData().getEncryptionKey().length > 0) {
  1973. try {
  1974. blob = symmetricEncryptionService.decrypt(
  1975. blob,
  1976. messageModel.getImageData().getEncryptionKey(),
  1977. messageModel.getImageData().getNonce()
  1978. );
  1979. } catch (Exception e) {
  1980. blob = null;
  1981. logger.error("Exception", e);
  1982. }
  1983. if (blob != null && blob.length > 0) {
  1984. try {
  1985. if (saveStrippedImage(blob, messageModel)) {
  1986. messageModel.getImageData().isDownloaded(true);
  1987. messageModel.writeDataModelToBody();
  1988. messageModelFactory.update(messageModel);
  1989. fireOnModifiedMessage(messageModel);
  1990. downloadService.complete(messageModel.getId(), message.getBlobId());
  1991. return messageModel;
  1992. }
  1993. } catch (Exception e) {
  1994. logger.error("Image save failed", e);
  1995. }
  1996. } else {
  1997. logger.error("Invalid blob");
  1998. }
  1999. } else {
  2000. logger.error("Blob is null");
  2001. }
  2002. downloadService.error(messageModel.getId());
  2003. }
  2004. messageModel.setSaved(true);
  2005. messageModelFactory.update(messageModel);
  2006. // download failed...let adapter know
  2007. fireOnModifiedMessage(messageModel);
  2008. return messageModel;
  2009. }
  2010. @Deprecated
  2011. private GroupMessageModel saveGroupMessage(GroupVideoMessage message, GroupMessageModel messageModel) throws Exception {
  2012. GroupModel groupModel = groupService.getByGroupMessage(message);
  2013. if (groupModel == null) {
  2014. logger.error("could not save a group message from an unknown group");
  2015. return null;
  2016. }
  2017. MessageReceiver messageReceiver = groupService.createReceiver(groupModel);
  2018. return (GroupMessageModel) saveVideoMessage(
  2019. messageReceiver,
  2020. message,
  2021. messageModel);
  2022. }
  2023. private GroupMessageModel saveGroupMessage(GroupAudioMessage message, GroupMessageModel messageModel) {
  2024. GroupModel groupModel = groupService.getByGroupMessage(message);
  2025. if (groupModel == null) {
  2026. return null;
  2027. }
  2028. MessageReceiver messageReceiver = groupService.createReceiver(groupModel);
  2029. return (GroupMessageModel) saveAudioMessage(
  2030. messageReceiver,
  2031. message,
  2032. messageModel);
  2033. }
  2034. @Nullable
  2035. @WorkerThread
  2036. private GroupMessageModel saveGroupMessage(GroupLocationMessage message, GroupMessageModel messageModel) {
  2037. GroupModel groupModel = groupService.getByGroupMessage(message);
  2038. boolean isNewMessage = false;
  2039. if (groupModel == null) {
  2040. return null;
  2041. }
  2042. MessageReceiver r = groupService.createReceiver(groupModel);
  2043. if (messageModel == null) {
  2044. messageModel = (GroupMessageModel) r.createLocalModel(MessageType.LOCATION, MessageContentsType.LOCATION, message.getDate());
  2045. cache(messageModel);
  2046. messageModel.setApiMessageId(message.getMessageId().toString());
  2047. messageModel.setMessageFlags(message.getMessageFlags());
  2048. messageModel.setOutbox(false);
  2049. messageModel.setIdentity(message.getFromIdentity());
  2050. r.saveLocalModel(messageModel);
  2051. isNewMessage = true;
  2052. }
  2053. // If the location model is missing an address, we perform a lookup based on the coordinates
  2054. @Nullable Poi effectivePoi = message.getPoi();
  2055. if (effectivePoi == null) {
  2056. try {
  2057. // Will result in "Unknown address" as a fallback value
  2058. final @NonNull String lookedUpPoiAddress = GeoLocationUtil.getAddressFromLocation(
  2059. context,
  2060. message.getLatitude(),
  2061. message.getLongitude()
  2062. );
  2063. effectivePoi = new Poi.Unnamed(lookedUpPoiAddress);
  2064. } catch (IOException ioException) {
  2065. logger.error("Exception", ioException);
  2066. }
  2067. }
  2068. messageModel.setLocationData(new LocationDataModel(
  2069. message.getLatitude(),
  2070. message.getLongitude(),
  2071. message.getAccuracy(),
  2072. effectivePoi
  2073. ));
  2074. messageModel.setSaved(true);
  2075. r.saveLocalModel(messageModel);
  2076. if (isNewMessage) {
  2077. fireOnNewMessage(messageModel);
  2078. } else {
  2079. fireOnModifiedMessage(messageModel);
  2080. }
  2081. return messageModel;
  2082. }
  2083. /**
  2084. * Process a 1:1 image message (0x02).
  2085. */
  2086. @Deprecated
  2087. private MessageModel saveBoxMessage(
  2088. @NonNull ImageMessage message,
  2089. MessageModel messageModel,
  2090. @NonNull ContactModel contactModel
  2091. ) {
  2092. logger.info("saveBoxMessage: {}", message.getMessageId());
  2093. logger.info("saveBoxMessage: {} - A", message.getMessageId());
  2094. MessageModelFactory messageModelFactory = databaseService.getMessageModelFactory();
  2095. logger.info("saveBoxMessage: {} - B", message.getMessageId());
  2096. if (messageModel == null) {
  2097. ContactMessageReceiver r = contactService.createReceiver(contactModel);
  2098. logger.info("saveBoxMessage: {} - C", message.getMessageId());
  2099. messageModel = r.createLocalModel(MessageType.IMAGE, MessageContentsType.IMAGE, message.getDate());
  2100. logger.info("saveBoxMessage: {} - D", message.getMessageId());
  2101. messageModel.setApiMessageId(message.getMessageId().toString());
  2102. messageModel.setMessageFlags(message.getMessageFlags());
  2103. messageModel.setOutbox(false);
  2104. messageModel.setIdentity(contactModel.getIdentity());
  2105. // Do not set an encryption key (asymmetric style)
  2106. messageModel.setImageData(new ImageDataModel(message.blobId, contactModel.getPublicKey(), message.nonce));
  2107. messageModel.setForwardSecurityMode(message.getForwardSecurityMode());
  2108. // Mark as saved to show message without image e.g.
  2109. messageModel.setSaved(true);
  2110. r.saveLocalModel(messageModel);
  2111. /*
  2112. //create the record
  2113. messageModelFactory.create(messageModel);
  2114. */
  2115. logger.info("saveBoxMessage: {} - E", message.getMessageId());
  2116. cache(messageModel);
  2117. }
  2118. fireOnNewMessage(messageModel);
  2119. logger.info("saveBoxMessage: {} - F", message.getMessageId());
  2120. if (shouldAutoDownload(MessageType.IMAGE) && !messageModel.getImageData().isDownloaded()) {
  2121. // Use download class to handle failures after downloads
  2122. byte[] imageBlob = downloadService.download(
  2123. messageModel.getId(),
  2124. message.blobId,
  2125. messageModel.getBlobScopeForDownload(),
  2126. messageModel.getBlobScopeForMarkAsDone(),
  2127. null
  2128. );
  2129. if (imageBlob != null) {
  2130. byte[] image = identityStore.decryptData(imageBlob, message.nonce, contactModel.getPublicKey());
  2131. if (image != null) {
  2132. try {
  2133. if (saveStrippedImage(image, messageModel)) {
  2134. // Mark as downloaded
  2135. messageModel.getImageData().isDownloaded(true);
  2136. messageModel.writeDataModelToBody();
  2137. messageModelFactory.update(messageModel);
  2138. //fire on new
  2139. fireOnModifiedMessage(messageModel);
  2140. // remove blob
  2141. downloadService.complete(messageModel.getId(), message.blobId);
  2142. return messageModel;
  2143. }
  2144. } catch (Exception e) {
  2145. logger.error("Image save failed", e);
  2146. }
  2147. } else {
  2148. logger.error("Unable to decrypt blob for message {}", messageModel.getId());
  2149. }
  2150. } else {
  2151. logger.error("Blob is null");
  2152. }
  2153. downloadService.error(messageModel.getId());
  2154. }
  2155. messageModel.setSaved(true);
  2156. messageModelFactory.update(messageModel);
  2157. // download failed...let adapter know
  2158. fireOnModifiedMessage(messageModel);
  2159. return messageModel;
  2160. }
  2161. /**
  2162. * Process a 1:1 video message (0x13).
  2163. */
  2164. @Deprecated
  2165. private MessageModel saveBoxMessage(
  2166. @NonNull VideoMessage message,
  2167. MessageModel messageModel,
  2168. @NonNull ContactModel contactModel
  2169. ) throws Exception {
  2170. MessageReceiver messageReceiver = contactService.createReceiver(contactModel);
  2171. return (MessageModel) saveVideoMessage(
  2172. messageReceiver,
  2173. message,
  2174. messageModel);
  2175. }
  2176. /**
  2177. * Process a 1:1 audio message (0x14).
  2178. */
  2179. @Deprecated
  2180. private MessageModel saveBoxMessage(
  2181. @NonNull AudioMessage message,
  2182. MessageModel messageModel,
  2183. @NonNull ContactModel contactModel
  2184. ) {
  2185. MessageReceiver messageReceiver = contactService.createReceiver(contactModel);
  2186. return (MessageModel) saveAudioMessage(
  2187. messageReceiver,
  2188. message,
  2189. messageModel);
  2190. }
  2191. private boolean saveStrippedImage(byte[] image, AbstractMessageModel messageModel) throws Exception {
  2192. boolean success = true;
  2193. // extract caption from exif data (legacy image format only) and strip all metadata, if any
  2194. try (ByteArrayOutputStream strippedImageOS = new ByteArrayOutputStream()) {
  2195. try (ByteArrayInputStream originalImageIS = new ByteArrayInputStream(image)) {
  2196. ExifInterface originalImageExif = new ExifInterface(originalImageIS);
  2197. if (messageModel.getType() == MessageType.IMAGE) {
  2198. String caption = originalImageExif.getUTF8StringAttribute(ExifInterface.TAG_ARTIST);
  2199. if (TestUtil.isEmptyOrNull(caption)) {
  2200. caption = originalImageExif.getUTF8StringAttribute(ExifInterface.TAG_USER_COMMENT);
  2201. }
  2202. if (!TestUtil.isEmptyOrNull(caption)) {
  2203. // strip trailing zero character from EXIF, if any
  2204. if (caption.charAt(caption.length() - 1) == '\u0000') {
  2205. caption = caption.substring(0, caption.length() - 1);
  2206. }
  2207. messageModel.setCaption(caption);
  2208. }
  2209. originalImageIS.reset();
  2210. }
  2211. // strip all exif data while saving
  2212. originalImageExif.saveAttributes(originalImageIS, strippedImageOS, true);
  2213. } catch (IOException e) {
  2214. logger.error("Exception", e);
  2215. success = false;
  2216. }
  2217. // check if a file already exist
  2218. fileService.removeMessageFiles(messageModel, true);
  2219. logger.info("Writing image file...");
  2220. if (success) {
  2221. // write stripped file
  2222. success = fileService.writeConversationMedia(messageModel, strippedImageOS.toByteArray());
  2223. } else {
  2224. // write original file
  2225. success = fileService.writeConversationMedia(messageModel, image);
  2226. }
  2227. if (success) {
  2228. logger.info("Image file successfully saved.");
  2229. } else {
  2230. logger.error("Image file save failed.");
  2231. }
  2232. messageModel.setSaved(true);
  2233. }
  2234. return success;
  2235. }
  2236. /**
  2237. * Process a 1:1 location message (0x10).
  2238. */
  2239. @WorkerThread
  2240. private MessageModel saveBoxMessage(
  2241. @NonNull LocationMessage message,
  2242. MessageModel messageModel,
  2243. @NonNull ContactModel contactModel
  2244. ) {
  2245. ContactMessageReceiver r = contactService.createReceiver(contactModel);
  2246. if (messageModel == null) {
  2247. messageModel = r.createLocalModel(MessageType.LOCATION, MessageContentsType.LOCATION, message.getDate());
  2248. cache(messageModel);
  2249. messageModel.setApiMessageId(message.getMessageId().toString());
  2250. messageModel.setMessageFlags(message.getMessageFlags());
  2251. messageModel.setOutbox(false);
  2252. }
  2253. messageModel.setIdentity(contactModel.getIdentity());
  2254. messageModel.setForwardSecurityMode(message.getForwardSecurityMode());
  2255. messageModel.setSaved(true);
  2256. messageModel.setLocationData(
  2257. new LocationDataModel(
  2258. message.getLatitude(),
  2259. message.getLongitude(),
  2260. message.getAccuracy(),
  2261. message.getPoi()
  2262. )
  2263. );
  2264. // We save the message model already here to ensure it is in the database in case the app
  2265. // gets killed before resolving the address.
  2266. databaseService.getMessageModelFactory().create(messageModel);
  2267. // If the location model is missing an address, we perform a lookup based on the coordinates
  2268. if (message.getPoi() == null) {
  2269. try {
  2270. // Will result in "Unknown address" as a fallback value
  2271. final @NonNull String lookedUpPoiAddress = GeoLocationUtil.getAddressFromLocation(
  2272. context,
  2273. message.getLatitude(),
  2274. message.getLongitude()
  2275. );
  2276. messageModel.setLocationData(
  2277. new LocationDataModel(
  2278. message.getLatitude(),
  2279. message.getLongitude(),
  2280. message.getAccuracy(),
  2281. new Poi.Unnamed(lookedUpPoiAddress)
  2282. )
  2283. );
  2284. // Update the db record
  2285. databaseService.getMessageModelFactory().update(messageModel);
  2286. } catch (IOException ioException) {
  2287. logger.error("Exception", ioException);
  2288. }
  2289. }
  2290. fireOnNewMessage(messageModel);
  2291. return messageModel;
  2292. }
  2293. @Override
  2294. public List<AbstractMessageModel> getMessagesForReceiver(@NonNull MessageReceiver receiver, MessageFilter messageFilter) {
  2295. return getMessagesForReceiver(receiver, messageFilter, true);
  2296. }
  2297. @Override
  2298. public List<AbstractMessageModel> getMessagesForReceiver(@NonNull MessageReceiver receiver, MessageFilter messageFilter, boolean appendUnreadMessage) {
  2299. List<AbstractMessageModel> messages = receiver.loadMessages(messageFilter);
  2300. if (!appendUnreadMessage) {
  2301. return messages;
  2302. }
  2303. switch (receiver.getType()) {
  2304. case MessageReceiver.Type_GROUP:
  2305. case MessageReceiver.Type_CONTACT:
  2306. return markFirstUnread(messages);
  2307. default:
  2308. return messages;
  2309. }
  2310. }
  2311. /**
  2312. * Mark the first unread Message
  2313. *
  2314. * @param messageModels Message Models
  2315. */
  2316. private List<AbstractMessageModel> markFirstUnread(List<AbstractMessageModel> messageModels) {
  2317. synchronized (messageModels) {
  2318. int firstUnreadMessagePosition = -1;
  2319. for (int n = 0; n < messageModels.size(); n++) {
  2320. AbstractMessageModel m = messageModels.get(n);
  2321. if (m != null) {
  2322. if (m.isOutbox()) {
  2323. break;
  2324. } else {
  2325. if (m.isRead()) {
  2326. break;
  2327. } else if (!m.isStatusMessage()) {
  2328. firstUnreadMessagePosition = n;
  2329. }
  2330. }
  2331. }
  2332. }
  2333. if (firstUnreadMessagePosition > -1) {
  2334. FirstUnreadMessageModel firstUnreadMessageModel = new FirstUnreadMessageModel();
  2335. firstUnreadMessageModel.setCreatedAt(messageModels.get(firstUnreadMessagePosition).getCreatedAt());
  2336. messageModels.add(firstUnreadMessagePosition + 1, firstUnreadMessageModel);
  2337. }
  2338. }
  2339. return messageModels;
  2340. }
  2341. @Override
  2342. public List<AbstractMessageModel> getMessagesForReceiver(@NonNull MessageReceiver receiver) {
  2343. return getMessagesForReceiver(receiver, null);
  2344. }
  2345. @Override
  2346. public List<AbstractMessageModel> getMessageForBallot(final BallotModel ballotModel) {
  2347. MessageReceiver receiver = ballotService.getReceiver(ballotModel);
  2348. if (receiver != null) {
  2349. List<AbstractMessageModel> ballotMessages = receiver.loadMessages(new MessageFilter() {
  2350. @Override
  2351. public long getPageSize() {
  2352. return 0;
  2353. }
  2354. @Override
  2355. public Integer getPageReferenceId() {
  2356. return null;
  2357. }
  2358. @Override
  2359. public boolean withStatusMessages() {
  2360. return false;
  2361. }
  2362. @Override
  2363. public boolean withUnsaved() {
  2364. return true;
  2365. }
  2366. @Override
  2367. public boolean onlyUnread() {
  2368. return false;
  2369. }
  2370. @Override
  2371. public boolean onlyDownloaded() {
  2372. return false;
  2373. }
  2374. @Override
  2375. public MessageType[] types() {
  2376. return new MessageType[]{
  2377. MessageType.BALLOT
  2378. };
  2379. }
  2380. @Override
  2381. public int[] contentTypes() {
  2382. return null;
  2383. }
  2384. @Override
  2385. public int[] displayTags() {
  2386. return null;
  2387. }
  2388. });
  2389. return Functional.filter(ballotMessages, (IPredicateNonNull<AbstractMessageModel>) type -> type.getBallotData().getBallotId() == ballotModel.getId());
  2390. }
  2391. return null;
  2392. }
  2393. private List<AbstractMessageModel> getContactMessagesForText(String query, boolean includeArchived, boolean starredOnly, boolean sortAscending) {
  2394. return databaseService.getMessageModelFactory().getMessagesByText(query, includeArchived, starredOnly, sortAscending);
  2395. }
  2396. private List<AbstractMessageModel> getGroupMessagesForText(String query, boolean includeArchived, boolean starredOnly, boolean sortAscending) {
  2397. return databaseService.getGroupMessageModelFactory().getMessagesByText(query, includeArchived, starredOnly, sortAscending);
  2398. }
  2399. @Override
  2400. public List<AbstractMessageModel> getMessagesForText(@Nullable String queryString, @MessageFilterFlags int filterFlags, boolean sortAscending) {
  2401. List<AbstractMessageModel> messageModels = new ArrayList<>();
  2402. boolean includeArchived = (filterFlags & FILTER_INCLUDE_ARCHIVED) == FILTER_INCLUDE_ARCHIVED;
  2403. boolean starredOnly = (filterFlags & FILTER_STARRED_ONLY) == FILTER_STARRED_ONLY;
  2404. if ((filterFlags & FILTER_CHATS) == FILTER_CHATS) {
  2405. messageModels.addAll(getContactMessagesForText(queryString, includeArchived,
  2406. starredOnly,
  2407. sortAscending));
  2408. }
  2409. if ((filterFlags & FILTER_GROUPS) == FILTER_GROUPS) {
  2410. messageModels.addAll(getGroupMessagesForText(queryString, includeArchived,
  2411. starredOnly,
  2412. sortAscending));
  2413. }
  2414. if (!messageModels.isEmpty()) {
  2415. if (sortAscending) {
  2416. Collections.sort(messageModels, (o1, o2) -> o1.getCreatedAt().compareTo(o2.getCreatedAt()));
  2417. } else {
  2418. Collections.sort(messageModels, (o1, o2) -> o2.getCreatedAt().compareTo(o1.getCreatedAt()));
  2419. }
  2420. }
  2421. return messageModels;
  2422. }
  2423. @Override
  2424. @WorkerThread
  2425. public int unstarAllMessages() {
  2426. return
  2427. databaseService.getMessageModelFactory().unstarAllMessages() +
  2428. databaseService.getGroupMessageModelFactory().unstarAllMessages();
  2429. }
  2430. @Override
  2431. @WorkerThread
  2432. public long countStarredMessages() throws SQLiteException {
  2433. return
  2434. databaseService.getMessageModelFactory().countStarredMessages() +
  2435. databaseService.getGroupMessageModelFactory().countStarredMessages();
  2436. }
  2437. @Override
  2438. @Nullable
  2439. public MessageModel getContactMessageModel(final Integer id) {
  2440. MessageModel model;
  2441. synchronized (contactMessageCache) {
  2442. model = Functional.select(contactMessageCache, type -> type.getId() == id);
  2443. }
  2444. if (model == null) {
  2445. model = databaseService.getMessageModelFactory().getById(id);
  2446. if (model != null) {
  2447. synchronized (contactMessageCache) {
  2448. contactMessageCache.add(model);
  2449. }
  2450. }
  2451. }
  2452. return model;
  2453. }
  2454. private @Nullable MessageModel getContactMessageModel(
  2455. @NonNull final String apiMessageId,
  2456. @NonNull ContactMessageReceiver messageReceiver
  2457. ) {
  2458. MessageModel model;
  2459. synchronized (contactMessageCache) {
  2460. model = Functional.select(
  2461. contactMessageCache,
  2462. messageModel -> apiMessageId.equals(messageModel.getApiMessageId())
  2463. && messageReceiver.getContact().getIdentity().equals(messageModel.getIdentity())
  2464. );
  2465. }
  2466. if (model == null) {
  2467. try {
  2468. model = databaseService.getMessageModelFactory().getByApiMessageIdAndIdentity(
  2469. new MessageId(Utils.hexStringToByteArray(apiMessageId)),
  2470. messageReceiver.getContact().getIdentity()
  2471. );
  2472. if (model != null) {
  2473. synchronized (contactMessageCache) {
  2474. contactMessageCache.add(model);
  2475. }
  2476. }
  2477. } catch (MessageId.BadMessageIdException ignore) {
  2478. logger.warn("Encountered invalid message ID in contact message");
  2479. }
  2480. }
  2481. return model;
  2482. }
  2483. @Nullable
  2484. @Override
  2485. public MessageModel getContactMessageModel(final String uid) {
  2486. return databaseService.getMessageModelFactory().getByUid(uid);
  2487. }
  2488. @Override
  2489. @Nullable
  2490. public GroupMessageModel getGroupMessageModel(final Integer id) {
  2491. synchronized (groupMessageCache) {
  2492. GroupMessageModel model = Functional.select(groupMessageCache, type -> type.getId() == id);
  2493. if (model == null) {
  2494. model = databaseService.getGroupMessageModelFactory().getById(id);
  2495. if (model != null) {
  2496. groupMessageCache.add(model);
  2497. }
  2498. }
  2499. return model;
  2500. }
  2501. }
  2502. @Nullable
  2503. @Override
  2504. public GroupMessageModel getGroupMessageModel(final String uid) {
  2505. return databaseService.getGroupMessageModelFactory().getByUid(uid);
  2506. }
  2507. private GroupMessageModel getGroupMessageModel(
  2508. @NonNull final String apiMessageId,
  2509. @NonNull GroupMessageReceiver messageReceiver
  2510. ) {
  2511. int groupId = messageReceiver.getGroup().getId();
  2512. synchronized (groupMessageCache) {
  2513. GroupMessageModel model = Functional.select(
  2514. groupMessageCache,
  2515. messageModel -> apiMessageId.equals(messageModel.getApiMessageId())
  2516. && groupId == messageModel.getGroupId()
  2517. );
  2518. if (model == null) {
  2519. try {
  2520. model = databaseService.getGroupMessageModelFactory().getByApiMessageIdAndGroupId(new MessageId(Utils.hexStringToByteArray(apiMessageId)), groupId);
  2521. if (model != null) {
  2522. groupMessageCache.add(model);
  2523. }
  2524. } catch (MessageId.BadMessageIdException ignore) {
  2525. logger.warn("Encountered invalid message ID in group message");
  2526. }
  2527. }
  2528. return model;
  2529. }
  2530. }
  2531. @Override
  2532. @Nullable
  2533. public DistributionListMessageModel getDistributionListMessageModel(long id) {
  2534. return databaseService.getDistributionListMessageModelFactory().getById(id);
  2535. }
  2536. private void fireOnNewMessage(final AbstractMessageModel messageModel) {
  2537. if (appLockService.isLocked()) {
  2538. //do not fire messages, wait until app is unlocked
  2539. appLockService.addOnLockAppStateChanged(locked -> !locked);
  2540. }
  2541. fireOnCreatedMessage(messageModel);
  2542. }
  2543. @Override
  2544. public MessageString getMessageString(AbstractMessageModel messageModel, int maxLength) {
  2545. return getMessageString(messageModel, maxLength, true);
  2546. }
  2547. @NonNull
  2548. @Override
  2549. public MessageString getMessageString(AbstractMessageModel messageModel, int maxLength, boolean withPrefix) {
  2550. boolean isPrivate;
  2551. String prefix = "";
  2552. if (messageModel instanceof GroupMessageModel) {
  2553. //append Username
  2554. if (withPrefix) {
  2555. prefix = NameUtil.getShortName(context, messageModel, contactService) + ": ";
  2556. }
  2557. final GroupModel groupModel = groupService.getById(((GroupMessageModel) messageModel).getGroupId());
  2558. isPrivate = conversationCategoryService.isPrivateChat(GroupUtil.getUniqueIdString(groupModel));
  2559. } else {
  2560. final String identity = messageModel.getIdentity();
  2561. if (identity != null) {
  2562. isPrivate = conversationCategoryService.isPrivateChat(ContactUtil.getUniqueIdString(messageModel.getIdentity()));
  2563. } else {
  2564. logger.error("The identity of the message model is null");
  2565. isPrivate = false;
  2566. }
  2567. }
  2568. if (isPrivate) {
  2569. return new MessageString(context.getString(R.string.new_messages_locked));
  2570. }
  2571. if (messageModel.isDeleted()) {
  2572. return new MessageString(context.getString(R.string.message_was_deleted));
  2573. }
  2574. switch (messageModel.getType()) {
  2575. case TEXT:
  2576. @Nullable String messageText = QuoteUtil.getMessageBody(messageModel, false);
  2577. String rawMessageText = prefix + messageText;
  2578. if (maxLength > 0 && messageText != null && messageText.length() > maxLength) {
  2579. messageText = messageText.substring(0, maxLength - 3) + "...";
  2580. }
  2581. return new MessageString(messageText, rawMessageText);
  2582. case VIDEO:
  2583. return new MessageString(prefix + context.getString(R.string.video_placeholder));
  2584. case LOCATION:
  2585. String locationString = prefix + context.getString(R.string.location_placeholder);
  2586. final @NonNull LocationDataModel locationDataModel = messageModel.getLocationData();
  2587. if (locationDataModel.poiNameOrNull != null) {
  2588. locationString += ": " + locationDataModel.poiNameOrNull;
  2589. }
  2590. return new MessageString(locationString);
  2591. case VOICEMESSAGE:
  2592. String messageString = prefix + context.getString(R.string.audio_placeholder);
  2593. messageString += " (" + StringConversionUtil.secondsToString(messageModel.getAudioData().getDuration(), false) + ")";
  2594. return new MessageString(messageString);
  2595. case FILE:
  2596. if (MimeUtil.isImageFile(messageModel.getFileData().getMimeType())) {
  2597. if (TestUtil.isEmptyOrNull(messageModel.getCaption())) {
  2598. return new MessageString(prefix + context.getString(R.string.image_placeholder));
  2599. } else {
  2600. return new MessageString(prefix + context.getString(R.string.image_placeholder) + ": " + messageModel.getFileData().getCaption());
  2601. }
  2602. } else if (MimeUtil.isVideoFile(messageModel.getFileData().getMimeType())) {
  2603. if (TestUtil.isEmptyOrNull(messageModel.getFileData().getCaption())) {
  2604. String durationString = messageModel.getFileData().getDurationString();
  2605. return new MessageString(prefix + context.getString(R.string.video_placeholder) + " (" + durationString + ")");
  2606. } else {
  2607. return new MessageString(prefix + context.getString(R.string.video_placeholder) + ": " + messageModel.getFileData().getCaption());
  2608. }
  2609. } else if (MimeUtil.isAudioFile(messageModel.getFileData().getMimeType())) {
  2610. if (TestUtil.isEmptyOrNull(messageModel.getFileData().getCaption())) {
  2611. String durationString = messageModel.getFileData().getDurationString();
  2612. if ("00:00".equals(durationString)) {
  2613. return new MessageString(prefix + context.getString(R.string.audio_placeholder));
  2614. } else {
  2615. return new MessageString(prefix + context.getString(R.string.audio_placeholder) + " (" + durationString + ")");
  2616. }
  2617. } else {
  2618. return new MessageString(prefix + context.getString(R.string.audio_placeholder) + ": " + messageModel.getFileData().getCaption());
  2619. }
  2620. } else {
  2621. if (TestUtil.isEmptyOrNull(messageModel.getFileData().getCaption())) {
  2622. return new MessageString(prefix + context.getString(R.string.file_placeholder) + ": " + messageModel.getFileData().getFileName());
  2623. } else {
  2624. return new MessageString(prefix + context.getString(R.string.file_placeholder) + ": " + messageModel.getFileData().getCaption());
  2625. }
  2626. }
  2627. case IMAGE:
  2628. if (TestUtil.isEmptyOrNull(messageModel.getCaption())) {
  2629. return new MessageString(prefix + context.getString(R.string.image_placeholder));
  2630. } else {
  2631. return new MessageString(prefix + context.getString(R.string.image_placeholder) + ": " + messageModel.getCaption());
  2632. }
  2633. case BALLOT:
  2634. return new MessageString(prefix + context.getString(R.string.ballot_placeholder) + ":" + BallotUtil.getNotificationString(context, messageModel));
  2635. case VOIP_STATUS:
  2636. return new MessageString(prefix + MessageUtil.getViewElement(context, messageModel).placeholder);
  2637. default:
  2638. return new MessageString(prefix);
  2639. }
  2640. }
  2641. @Override
  2642. public void saveIncomingServerMessage(final ServerMessageModel msg) {
  2643. // Store server message into database
  2644. databaseService.getServerMessageModelFactory().storeServerMessageModel(msg);
  2645. // Show as alert
  2646. ListenerManager.serverMessageListeners.handle(listener -> {
  2647. if (msg.getType() == ServerMessageModel.TYPE_ALERT) {
  2648. listener.onAlert(msg);
  2649. } else {
  2650. listener.onError(msg);
  2651. }
  2652. });
  2653. }
  2654. @Override
  2655. public boolean downloadMediaMessage(
  2656. @Nullable AbstractMessageModel mediaMessageModel,
  2657. @Nullable ProgressListener progressListener
  2658. ) throws Exception {
  2659. if (!MessageUtil.hasDataFile(mediaMessageModel)) {
  2660. throw new ThreemaException("message is not a media message");
  2661. }
  2662. MediaMessageDataInterface data = getDataForMessageType(mediaMessageModel);
  2663. if (data != null && !data.isDownloaded()) {
  2664. if (downloadAndWriteMediaData(mediaMessageModel, data, progressListener)) {
  2665. setDownloadCompleted(mediaMessageModel, data);
  2666. saveImagesAndVideosToGalleryIfEnabled(mediaMessageModel, data);
  2667. return true;
  2668. } else {
  2669. logger.error("Decryption failed");
  2670. this.downloadService.error(mediaMessageModel.getId());
  2671. throw new ThreemaException("Decryption failed");
  2672. }
  2673. }
  2674. return false;
  2675. }
  2676. private @Nullable MediaMessageDataInterface getDataForMessageType(
  2677. @NonNull AbstractMessageModel mediaMessageModel
  2678. ) {
  2679. switch (mediaMessageModel.getType()) {
  2680. case IMAGE:
  2681. return mediaMessageModel.getImageData();
  2682. case VIDEO:
  2683. return mediaMessageModel.getVideoData();
  2684. case VOICEMESSAGE:
  2685. return mediaMessageModel.getAudioData();
  2686. case FILE:
  2687. return mediaMessageModel.getFileData();
  2688. default:
  2689. return null;
  2690. }
  2691. }
  2692. private @NonNull
  2693. byte[] getNonceForMessageType(@NonNull MessageType messageType) throws ThreemaException {
  2694. switch (messageType) {
  2695. case IMAGE:
  2696. return ProtocolDefines.IMAGE_NONCE;
  2697. case VIDEO:
  2698. return ProtocolDefines.VIDEO_NONCE;
  2699. case VOICEMESSAGE:
  2700. return ProtocolDefines.AUDIO_NONCE;
  2701. case FILE:
  2702. return ProtocolDefines.FILE_NONCE;
  2703. default:
  2704. throw new ThreemaException("Could not get nonce for messageType=" + messageType);
  2705. }
  2706. }
  2707. private boolean downloadAndWriteMediaData(
  2708. @NonNull AbstractMessageModel mediaMessageModel,
  2709. @NonNull MediaMessageDataInterface data,
  2710. @Nullable ProgressListener progressListener
  2711. ) throws ThreemaException {
  2712. if (mediaMessageModel.getType() != MessageType.IMAGE) {
  2713. File messageFile = fileService.getMessageFile(mediaMessageModel);
  2714. if (messageFile != null && messageFile.exists() && messageFile.length() > NaCl.BOXOVERHEAD) {
  2715. // hack: do not re-download a blob that's already present on the file system
  2716. return true;
  2717. }
  2718. }
  2719. // If multi-device is active, we always mark as done. Otherwise we do not mark as done if its a group message
  2720. boolean shouldMarkAsDone = multiDeviceManager.isMultiDeviceActive() || !(mediaMessageModel instanceof GroupMessageModel);
  2721. @Nullable BlobScope blobScopeMarkAsDone = null;
  2722. if (shouldMarkAsDone) {
  2723. blobScopeMarkAsDone = mediaMessageModel.getBlobScopeForMarkAsDone();
  2724. }
  2725. byte[] blob = downloadService.download(
  2726. mediaMessageModel.getId(),
  2727. data.getBlobId(),
  2728. mediaMessageModel.getBlobScopeForDownload(),
  2729. blobScopeMarkAsDone,
  2730. progressListener
  2731. );
  2732. if (blob == null || blob.length < NaCl.BOXOVERHEAD) {
  2733. logger.error("Blob for message {} is empty", mediaMessageModel.getApiMessageId());
  2734. downloadService.error(mediaMessageModel.getId());
  2735. // blob download failed or empty or canceled
  2736. throw new ThreemaException("failed to download message");
  2737. }
  2738. boolean success = mediaMessageModel.getType() != MessageType.IMAGE
  2739. ? decryptNonImageMediaDataAndWriteConversationMedia(mediaMessageModel, data, blob)
  2740. : decryptImageAndWriteConversationMedia(mediaMessageModel, blob);
  2741. if (success && !fileService.hasMessageThumbnail(mediaMessageModel)) {
  2742. createAndWriteMediaThumbnail(mediaMessageModel);
  2743. }
  2744. return success;
  2745. }
  2746. private void setDownloadCompleted(@NonNull AbstractMessageModel mediaMessageModel, @NonNull MediaMessageDataInterface data) {
  2747. if (mediaMessageModel.getType() == MessageType.IMAGE) {
  2748. mediaMessageModel.getImageData().isDownloaded(true);
  2749. } else if (mediaMessageModel.getType() == MessageType.VIDEO) {
  2750. mediaMessageModel.getVideoData().isDownloaded(true);
  2751. } else if (mediaMessageModel.getType() == MessageType.VOICEMESSAGE) {
  2752. mediaMessageModel.getAudioData().isDownloaded(true);
  2753. } else if (mediaMessageModel.getType() == MessageType.FILE) {
  2754. mediaMessageModel.getFileData().isDownloaded(true);
  2755. }
  2756. mediaMessageModel.writeDataModelToBody();
  2757. save(mediaMessageModel);
  2758. fireOnModifiedMessage(mediaMessageModel);
  2759. downloadService.complete(mediaMessageModel.getId(), data.getBlobId());
  2760. }
  2761. private void saveImagesAndVideosToGalleryIfEnabled(@NonNull AbstractMessageModel mediaMessageModel, @NonNull MediaMessageDataInterface data) {
  2762. if (preferenceService != null
  2763. && preferenceService.isSaveMedia()
  2764. && isImageOrVideoFile(mediaMessageModel, data)) {
  2765. boolean isPrivate = mediaMessageModel instanceof GroupMessageModel
  2766. ? conversationCategoryService.isPrivateChat(GroupUtil.getUniqueIdString(((GroupMessageModel) mediaMessageModel).getGroupId()))
  2767. : conversationCategoryService.isPrivateChat(ContactUtil.getUniqueIdString(mediaMessageModel.getIdentity()));
  2768. if (!isPrivate) {
  2769. fileService.saveMedia(null, null, new CopyOnWriteArrayList<>(Collections.singletonList(mediaMessageModel)), true);
  2770. }
  2771. }
  2772. }
  2773. private boolean isImageOrVideoFile(@NonNull AbstractMessageModel mediaMessageModel, @NonNull MediaMessageDataInterface data) {
  2774. MessageType type = mediaMessageModel.getType();
  2775. return type == MessageType.IMAGE
  2776. || type == MessageType.VIDEO
  2777. || (type == MessageType.FILE && FileUtil.isImageOrVideoFile((FileDataModel) data));
  2778. }
  2779. private boolean decryptNonImageMediaDataAndWriteConversationMedia(
  2780. @NonNull AbstractMessageModel messageModel,
  2781. @NonNull MediaMessageDataInterface data,
  2782. @NonNull byte[] blob
  2783. ) throws ThreemaException {
  2784. logger.info("Decrypting blob for message {}", messageModel.getApiMessageId());
  2785. byte[] nonce = getNonceForMessageType(messageModel.getType());
  2786. if (symmetricEncryptionService.decryptInplace(blob, data.getEncryptionKey(), nonce)) {
  2787. logger.info("Write conversation media for message {}", messageModel.getApiMessageId());
  2788. // save the file
  2789. try {
  2790. if (fileService.writeConversationMedia(messageModel, blob, 0, blob.length - NaCl.BOXOVERHEAD, true)) {
  2791. logger.info("Media for message {} successfully saved.", messageModel.getApiMessageId());
  2792. return true;
  2793. }
  2794. } catch (Exception e) {
  2795. logger.warn("Unable to save media");
  2796. downloadService.error(messageModel.getId());
  2797. throw new ThreemaException("Unable to save media");
  2798. }
  2799. }
  2800. return false;
  2801. }
  2802. private boolean decryptImageAndWriteConversationMedia(
  2803. @NonNull AbstractMessageModel messageModel,
  2804. @NonNull byte[] blob
  2805. ) {
  2806. ImageDataModel imageData = messageModel.getImageData();
  2807. byte[] image = messageModel instanceof GroupMessageModel
  2808. ? NaCl.symmetricDecryptData(blob, imageData.getEncryptionKey(), ProtocolDefines.IMAGE_NONCE)
  2809. : identityStore.decryptData(blob, imageData.getNonce(), imageData.getEncryptionKey());
  2810. if (image != null && image.length > 0) {
  2811. try {
  2812. // save the file
  2813. return saveStrippedImage(image, messageModel);
  2814. } catch (Exception e) {
  2815. logger.error("Exception", e);
  2816. }
  2817. }
  2818. return false;
  2819. }
  2820. private void createAndWriteMediaThumbnail(@NonNull AbstractMessageModel messageModel) {
  2821. if (!MessageUtil.canHaveThumbnailFile(messageModel)) {
  2822. // ignore messages that cannot have a thumbnail
  2823. return;
  2824. }
  2825. try {
  2826. File file = fileService.getDecryptedMessageFile(messageModel);
  2827. byte[] thumbnailData = ThumbnailUtil.generateThumbnailData(context, getMimeTypeString(messageModel), file);
  2828. if (thumbnailData != null) {
  2829. fileService.writeConversationMediaThumbnail(messageModel, thumbnailData);
  2830. }
  2831. } catch (Exception e) {
  2832. logger.error("Could not write conversation media thumbnail", e);
  2833. }
  2834. }
  2835. @Override
  2836. public boolean cancelMessageDownload(AbstractMessageModel messageModel) {
  2837. return downloadService.cancel(messageModel.getId());
  2838. }
  2839. private void fireOnCreatedMessage(final AbstractMessageModel messageModel) {
  2840. logger.debug("fireOnCreatedMessage for message {}", messageModel.getApiMessageId());
  2841. ListenerManager.messageListeners.handle(listener -> listener.onNew(messageModel));
  2842. }
  2843. private void fireOnModifiedMessage(final AbstractMessageModel messageModel) {
  2844. ListenerManager.messageListeners.handle(listener -> {
  2845. List<AbstractMessageModel> list = new ArrayList<>();
  2846. list.add(messageModel);
  2847. listener.onModified(list);
  2848. });
  2849. }
  2850. private void fireOnMessageDeletedForAll(final AbstractMessageModel messageModel) {
  2851. ListenerManager.messageDeletedForAllListener.handle(listener -> listener.onDeletedForAll(messageModel));
  2852. }
  2853. private void fireOnEditMessage(final AbstractMessageModel messageModel) {
  2854. ListenerManager.editMessageListener.handle(listener -> listener.onEdit(messageModel));
  2855. }
  2856. private void fireOnRemovedMessage(final AbstractMessageModel messageModel) {
  2857. ListenerManager.messageListeners.handle(listener -> listener.onRemoved(messageModel));
  2858. }
  2859. private void setMessageLoadingFinished(AbstractMessageModel messageModel) {
  2860. loadingProgress.delete(messageModel.getId());
  2861. cancelUploader(messageModel);
  2862. }
  2863. private void updateMessageLoadingProgress(final AbstractMessageModel messageModel, final int progress) {
  2864. loadingProgress.put(messageModel.getId(), progress);
  2865. //handle progress
  2866. ListenerManager.messageListeners.handle(listener -> listener.onProgressChanged(messageModel, progress));
  2867. }
  2868. @Override
  2869. public void removeAll() throws SQLException, IOException, ThreemaException {
  2870. //use the fast way
  2871. databaseService.getMessageModelFactory().deleteAll();
  2872. databaseService.getGroupMessageModelFactory().deleteAll();
  2873. databaseService.getDistributionListMessageModelFactory().deleteAll();
  2874. //clear all caches
  2875. synchronized (contactMessageCache) {
  2876. contactMessageCache.clear();
  2877. }
  2878. //clear all caches
  2879. synchronized (groupMessageCache) {
  2880. groupMessageCache.clear();
  2881. }
  2882. //clear all caches
  2883. synchronized (distributionListMessageCache) {
  2884. distributionListMessageCache.clear();
  2885. }
  2886. //clear all files in app Path
  2887. fileService.clearDirectory(fileService.getAppDataPath(), false);
  2888. }
  2889. @Override
  2890. public void save(final AbstractMessageModel messageModel) {
  2891. if (messageModel != null) {
  2892. if (messageModel instanceof MessageModel) {
  2893. synchronized (contactMessageCache) {
  2894. databaseService.getMessageModelFactory().createOrUpdate(
  2895. (MessageModel) messageModel
  2896. );
  2897. // Update the cache
  2898. Iterator<MessageModel> iterator = contactMessageCache.iterator();
  2899. while (iterator.hasNext()) {
  2900. MessageModel cached = iterator.next();
  2901. if (cached.getId() == messageModel.getId() && cached != messageModel) {
  2902. // Remove old message model from cache if not the same object
  2903. iterator.remove();
  2904. }
  2905. }
  2906. }
  2907. } else if (messageModel instanceof GroupMessageModel) {
  2908. synchronized (groupMessageCache) {
  2909. databaseService.getGroupMessageModelFactory().createOrUpdate(
  2910. (GroupMessageModel) messageModel);
  2911. //remove "old" message models from cache
  2912. for (GroupMessageModel m : Functional.filter(groupMessageCache, (IPredicateNonNull<GroupMessageModel>) type -> type.getId() == messageModel.getId() && messageModel != type)) {
  2913. logger.debug("Updating cached data for group message model {}", messageModel.getApiMessageId());
  2914. m.copyFrom((GroupMessageModel) messageModel);
  2915. }
  2916. }
  2917. } else if (messageModel instanceof DistributionListMessageModel) {
  2918. synchronized (distributionListMessageCache) {
  2919. databaseService.getDistributionListMessageModelFactory().createOrUpdate(
  2920. (DistributionListMessageModel) messageModel);
  2921. //remove "old" message models from cache
  2922. for (DistributionListMessageModel m : Functional.filter(distributionListMessageCache, (IPredicateNonNull<DistributionListMessageModel>) type -> type.getId() == messageModel.getId() && messageModel != type)) {
  2923. //remove cached unsaved object
  2924. logger.debug("copy from distribution list message model fix");
  2925. m.copyFrom(messageModel);
  2926. }
  2927. }
  2928. }
  2929. // Cache the element for more actions
  2930. cache(messageModel);
  2931. }
  2932. }
  2933. @Override
  2934. public long getTotalMessageCount() {
  2935. //simple count
  2936. return databaseService.getMessageModelFactory().count()
  2937. + databaseService.getGroupMessageModelFactory().count()
  2938. + databaseService.getDistributionListMessageModelFactory().count();
  2939. }
  2940. @NonNull
  2941. private String getMimeTypeString(AbstractMessageModel model) {
  2942. switch (model.getType()) {
  2943. case VIDEO:
  2944. return MimeUtil.MIME_TYPE_VIDEO;
  2945. case FILE:
  2946. return model.getFileData().getMimeType();
  2947. case VOICEMESSAGE:
  2948. return MimeUtil.MIME_TYPE_AUDIO;
  2949. case IMAGE:
  2950. return MimeUtil.MIME_TYPE_IMAGE_JPEG;
  2951. default:
  2952. return MimeUtil.MIME_TYPE_ANY;
  2953. }
  2954. }
  2955. private String getLeastCommonDenominatorMimeType(ArrayList<AbstractMessageModel> models) {
  2956. String mimeType = getMimeTypeString(models.get(0));
  2957. if (models.size() > 1) {
  2958. for (int i = 1; i < models.size(); i++) {
  2959. mimeType = MimeUtil.getCommonMimeType(mimeType, getMimeTypeString(models.get(i)));
  2960. }
  2961. }
  2962. return mimeType;
  2963. }
  2964. @Override
  2965. public boolean shareMediaMessages(final Context context, ArrayList<AbstractMessageModel> models, ArrayList<Uri> shareFileUris, String caption) {
  2966. if (TestUtil.required(context, models, shareFileUris)) {
  2967. if (!models.isEmpty() && !shareFileUris.isEmpty()) {
  2968. Intent intent;
  2969. if (models.size() == 1) {
  2970. AbstractMessageModel model = models.get(0);
  2971. Uri shareFileUri = shareFileUris.get(0);
  2972. if (shareFileUri == null) {
  2973. logger.info("No file to share");
  2974. return false;
  2975. }
  2976. intent = new Intent(Intent.ACTION_SEND);
  2977. intent.putExtra(Intent.EXTRA_STREAM, shareFileUri);
  2978. intent.setType(getMimeTypeString(model));
  2979. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(shareFileUri.getScheme())) {
  2980. intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  2981. }
  2982. if (!TestUtil.isEmptyOrNull(caption)) {
  2983. intent.putExtra(Intent.EXTRA_TEXT, caption);
  2984. }
  2985. } else {
  2986. intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
  2987. intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, shareFileUris);
  2988. Uri firstShareFileUri = shareFileUris.get(0);
  2989. intent.setType(getLeastCommonDenominatorMimeType(models));
  2990. if (firstShareFileUri != null) {
  2991. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(firstShareFileUri.getScheme())) {
  2992. intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  2993. }
  2994. }
  2995. }
  2996. try {
  2997. context.startActivity(Intent.createChooser(intent, context.getResources().getText(R.string.share_via)));
  2998. return true;
  2999. } catch (ActivityNotFoundException e) {
  3000. // make sure Toast runs in UI thread
  3001. RuntimeUtil.runOnUiThread(() -> Toast.makeText(context, R.string.no_activity_for_mime_type, Toast.LENGTH_SHORT).show());
  3002. }
  3003. }
  3004. }
  3005. return false;
  3006. }
  3007. @Override
  3008. public boolean viewMediaMessage(final Context context, AbstractMessageModel model, Uri uri) {
  3009. if (TestUtil.required(context, model, uri)) {
  3010. Intent intent = new Intent(Intent.ACTION_VIEW);
  3011. String mimeType = getMimeTypeString(model);
  3012. if (MimeUtil.isImageFile(mimeType)) {
  3013. // some viewers cannot handle image/gif - give them a generic mime type
  3014. mimeType = MimeUtil.MIME_TYPE_IMAGE;
  3015. }
  3016. intent.setDataAndType(uri, mimeType);
  3017. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
  3018. intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
  3019. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
  3020. intent.setClipData(ClipData.newRawUri("", uri));
  3021. intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  3022. }
  3023. } else if (!(context instanceof Activity)) {
  3024. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  3025. }
  3026. try {
  3027. context.startActivity(intent);
  3028. } catch (ActivityNotFoundException e) {
  3029. // make sure Toast runs in UI thread
  3030. RuntimeUtil.runOnUiThread(() -> Toast.makeText(context, R.string.no_activity_for_mime_type, Toast.LENGTH_SHORT).show());
  3031. } catch (SecurityException e) {
  3032. logger.error("Error firing ACTION_VIEW intent", e);
  3033. RuntimeUtil.runOnUiThread(() -> Toast.makeText(context, context.getString(R.string.no_activity_for_mime_type) + " " + e.getMessage(), Toast.LENGTH_SHORT).show());
  3034. }
  3035. }
  3036. return false;
  3037. }
  3038. @Override
  3039. public boolean shareTextMessage(Context context, AbstractMessageModel messageModel) {
  3040. if (messageModel != null) {
  3041. String text = "";
  3042. Intent intent = new Intent();
  3043. if (messageModel.getType() == MessageType.LOCATION) {
  3044. Uri locationUri = GeoLocationUtil.getLocationUri(messageModel);
  3045. final @NonNull LocationDataModel locationDataModel = messageModel.getLocationData();
  3046. if (locationDataModel.poiAddressOrNull != null) {
  3047. text = locationDataModel.poiAddressOrNull + " - ";
  3048. }
  3049. text += locationUri.toString();
  3050. } else {
  3051. text = QuoteUtil.getMessageBody(messageModel, false);
  3052. }
  3053. intent.setAction(Intent.ACTION_SEND);
  3054. intent.putExtra(android.content.Intent.EXTRA_TEXT, text);
  3055. intent.setType(MimeUtil.MIME_TYPE_TEXT);
  3056. try {
  3057. context.startActivity(Intent.createChooser(intent, context.getResources().getText(R.string.share_via)));
  3058. } catch (Exception e) {
  3059. Toast.makeText(context, R.string.no_activity_for_mime_type, Toast.LENGTH_LONG).show();
  3060. logger.error("Exception", e);
  3061. }
  3062. }
  3063. return false;
  3064. }
  3065. @Override
  3066. public void markConversationAsRead(MessageReceiver messageReceiver, NotificationService notificationService) {
  3067. @SuppressWarnings("unchecked")
  3068. List<AbstractMessageModel> unreadMessages = messageReceiver.loadMessages(new MessageService.MessageFilter() {
  3069. @Override
  3070. public long getPageSize() {
  3071. return 0;
  3072. }
  3073. @Override
  3074. public Integer getPageReferenceId() {
  3075. return null;
  3076. }
  3077. @Override
  3078. public boolean withStatusMessages() {
  3079. return false;
  3080. }
  3081. @Override
  3082. public boolean withUnsaved() {
  3083. return false;
  3084. }
  3085. @Override
  3086. public boolean onlyUnread() {
  3087. return true;
  3088. }
  3089. @Override
  3090. public boolean onlyDownloaded() {
  3091. return false;
  3092. }
  3093. @Override
  3094. public MessageType[] types() {
  3095. return null;
  3096. }
  3097. @Override
  3098. public int[] contentTypes() {
  3099. return null;
  3100. }
  3101. @Override
  3102. public int[] displayTags() {
  3103. return null;
  3104. }
  3105. });
  3106. new MarkAsReadRoutine(this, notificationService)
  3107. .run(unreadMessages, messageReceiver);
  3108. notificationService.cancel(messageReceiver);
  3109. }
  3110. @Override
  3111. public AbstractMessageModel getMessageModelFromId(int id, String type) {
  3112. if (id != 0 && !TestUtil.isEmptyOrNull(type)) {
  3113. if (type.equals(MessageModel.class.toString())) {
  3114. return getContactMessageModel(id);
  3115. } else if (type.equals(GroupMessageModel.class.toString())) {
  3116. return getGroupMessageModel(id);
  3117. } else if (type.equals(DistributionListMessageModel.class.toString())) {
  3118. return getDistributionListMessageModel(id);
  3119. }
  3120. }
  3121. return null;
  3122. }
  3123. @Override
  3124. @Nullable
  3125. public AbstractMessageModel getMessageModelByApiMessageIdAndReceiver(
  3126. @Nullable String apiMessageId,
  3127. @NonNull MessageReceiver messageReceiver
  3128. ) {
  3129. if (apiMessageId != null) {
  3130. if (messageReceiver instanceof ContactMessageReceiver) {
  3131. return getContactMessageModel(apiMessageId, (ContactMessageReceiver) messageReceiver);
  3132. } else if (messageReceiver instanceof GroupMessageReceiver) {
  3133. return getGroupMessageModel(apiMessageId, (GroupMessageReceiver) messageReceiver);
  3134. } else if (messageReceiver instanceof DistributionListMessageReceiver) {
  3135. // We cannot return a message model with a certain api message id for distribution
  3136. // lists, because the api message id is null for all distribution list messages
  3137. return null;
  3138. }
  3139. }
  3140. return null;
  3141. }
  3142. /*******************************************************************************************
  3143. * Uploader Cache (used to cancel running downloads)
  3144. *******************************************************************************************/
  3145. private final Map<String, BlobUploader> uploaders = new ArrayMap<>();
  3146. private final Map<String, WeakReference<VideoTranscoder>> videoTranscoders = new ArrayMap<>();
  3147. /**
  3148. * Create a new BlobUploader. An existing uploader will be canceled.
  3149. */
  3150. @NonNull
  3151. private BlobUploader initUploader(
  3152. AbstractMessageModel messageModel,
  3153. byte[] data,
  3154. @NonNull MessageReceiver<?> messageReceiver
  3155. ) throws ThreemaException {
  3156. synchronized (uploaders) {
  3157. final @NonNull String key = cancelUploader(messageModel);
  3158. final boolean isNotesGroup =
  3159. messageReceiver instanceof GroupMessageReceiver &&
  3160. groupService.isNotesGroup(((GroupMessageReceiver) messageReceiver).getGroup());
  3161. boolean shouldPersist = shouldPersistUploadForMessage(messageModel, isNotesGroup);
  3162. // If the message is sent to a notes group, the blob scope must not be "public"
  3163. @NonNull BlobScope blobScope = isNotesGroup ? BlobScope.Local.INSTANCE : BlobScope.Public.INSTANCE;
  3164. BlobUploader uploader = apiService.createUploader(
  3165. data,
  3166. shouldPersist,
  3167. blobScope
  3168. );
  3169. uploaders.put(key, uploader);
  3170. logger.debug("Created new uploader for message {}, persist={}", key, shouldPersist);
  3171. return uploader;
  3172. }
  3173. }
  3174. private boolean shouldPersistUploadForMessage(@NonNull AbstractMessageModel messageModel, boolean isNotesGroup) {
  3175. if (messageModel instanceof MessageModel) {
  3176. // 1:1 messages do not need to be persisted
  3177. return false;
  3178. } else if (messageModel instanceof GroupMessageModel) {
  3179. // Messages in groups need to be persisted if it is not a notes group
  3180. return !isNotesGroup;
  3181. } else if (messageModel instanceof DistributionListMessageModel) {
  3182. // Messages in distribution lists must be persisted
  3183. return true;
  3184. } else {
  3185. // This cannot happen
  3186. logger.error("Unexpected message model. Cannot determine whether it should be persisted or not");
  3187. return false;
  3188. }
  3189. }
  3190. @NonNull
  3191. private String getLoaderKey(@NonNull AbstractMessageModel messageModel) {
  3192. return messageModel.getClass() + "-" + messageModel.getUid();
  3193. }
  3194. /**
  3195. * Cancel an existing BlobUploader for the same {@code messageModel}
  3196. */
  3197. @NonNull
  3198. private String cancelUploader(@NonNull AbstractMessageModel messageModel) {
  3199. synchronized (uploaders) {
  3200. String key = getLoaderKey(messageModel);
  3201. final @Nullable BlobUploader blobUploader = uploaders.get(key);
  3202. if (blobUploader != null) {
  3203. logger.debug("cancel upload of message {}", key);
  3204. blobUploader.cancel();
  3205. uploaders.remove(key);
  3206. }
  3207. return key;
  3208. }
  3209. }
  3210. /**
  3211. * cancel an existing video transcoding
  3212. */
  3213. private String cancelTranscoding(AbstractMessageModel messageModel) {
  3214. synchronized (videoTranscoders) {
  3215. String key = getLoaderKey(messageModel);
  3216. if (videoTranscoders.containsKey(key)) {
  3217. logger.debug("cancel transcoding of message {}", key);
  3218. WeakReference<VideoTranscoder> videoTranscoderRef = videoTranscoders.get(key);
  3219. if (videoTranscoderRef != null) {
  3220. if (videoTranscoderRef.get() != null) {
  3221. videoTranscoderRef.get().cancel();
  3222. }
  3223. }
  3224. videoTranscoders.remove(key);
  3225. }
  3226. return key;
  3227. }
  3228. }
  3229. @Override
  3230. public void cancelMessageUpload(AbstractMessageModel messageModel) {
  3231. updateOutgoingMessageState(messageModel, MessageState.SENDFAILED, new Date());
  3232. if (messageSendingService != null) {
  3233. messageSendingService.abort(messageModel.getUid());
  3234. }
  3235. removeSendMachine(messageModel);
  3236. cancelUploader(messageModel);
  3237. }
  3238. @Override
  3239. public void cancelVideoTranscoding(AbstractMessageModel messageModel) {
  3240. updateOutgoingMessageState(messageModel, MessageState.SENDFAILED, new Date());
  3241. removeSendMachine(messageModel);
  3242. cancelTranscoding(messageModel);
  3243. }
  3244. /******************************************************************************************
  3245. * Sending Message Machine
  3246. * * Handling sending steps of image/video/audio or file messages
  3247. * * Can be aborted
  3248. ******************************************************************************************/
  3249. public final Map<String, SendMachine> sendMachineInstances = new HashMap<>();
  3250. /**
  3251. * Remove a instantiated sendmachine if exists
  3252. */
  3253. public void removeSendMachine(SendMachine sendMachine) {
  3254. if (sendMachine != null) {
  3255. sendMachine.abort();
  3256. //remove from instances
  3257. synchronized (sendMachineInstances) {
  3258. for (Iterator<Map.Entry<String, SendMachine>> it = sendMachineInstances.entrySet().iterator(); it.hasNext(); ) {
  3259. Map.Entry<String, SendMachine> entry = it.next();
  3260. if (entry.getValue() == sendMachine) {
  3261. logger.debug("remove send machine from instance map");
  3262. it.remove();
  3263. }
  3264. }
  3265. }
  3266. }
  3267. }
  3268. public void removeSendMachine(AbstractMessageModel messageModel) {
  3269. if (messageModel == null) {
  3270. //ignore
  3271. return;
  3272. }
  3273. removeSendMachine(getSendMachine(messageModel, false));
  3274. }
  3275. /**
  3276. * get or create a existing send machine
  3277. */
  3278. public SendMachine getSendMachine(AbstractMessageModel abstractMessageModel) {
  3279. return getSendMachine(abstractMessageModel, true);
  3280. }
  3281. /**
  3282. * get a send machine or create one (and cache into machine instances)
  3283. * can return NULL
  3284. */
  3285. public SendMachine getSendMachine(AbstractMessageModel abstractMessageModel, boolean createIfNotExists) {
  3286. synchronized (sendMachineInstances) {
  3287. //be sure to "generate" a unique key
  3288. String key = abstractMessageModel.getClass() + "-" + abstractMessageModel.getUid();
  3289. SendMachine instance = null;
  3290. if (sendMachineInstances.containsKey(key)) {
  3291. instance = sendMachineInstances.get(key);
  3292. } else if (createIfNotExists) {
  3293. instance = new SendMachine();
  3294. sendMachineInstances.put(key, instance);
  3295. }
  3296. return instance;
  3297. }
  3298. }
  3299. interface SendMachineProcess {
  3300. void run() throws Exception;
  3301. }
  3302. private static class SendMachine {
  3303. private int nextStep = 0;
  3304. private int currentStep = 0;
  3305. private boolean aborted = false;
  3306. public SendMachine reset() {
  3307. currentStep = 0;
  3308. return this;
  3309. }
  3310. public SendMachine abort() {
  3311. logger.debug("SendMachine: Aborted");
  3312. aborted = true;
  3313. return this;
  3314. }
  3315. public SendMachine next(SendMachineProcess process) throws Exception {
  3316. if (aborted) {
  3317. logger.debug("SendMachine: Ignore step, aborted");
  3318. //do nothing
  3319. return this;
  3320. }
  3321. if (nextStep == currentStep++) {
  3322. try {
  3323. if (process != null) {
  3324. process.run();
  3325. }
  3326. nextStep++;
  3327. } catch (Exception x) {
  3328. logger.error("SendMachine: Exception", x);
  3329. throw x;
  3330. }
  3331. }
  3332. return this;
  3333. }
  3334. }
  3335. @Override
  3336. public MessageReceiver getMessageReceiver(AbstractMessageModel messageModel) throws ThreemaException {
  3337. if (messageModel instanceof MessageModel) {
  3338. return contactService.createReceiver(contactService.getByIdentity(messageModel.getIdentity()));
  3339. } else if (messageModel instanceof GroupMessageModel) {
  3340. return groupService.createReceiver(groupService.getById(((GroupMessageModel) messageModel).getGroupId()));
  3341. } else if (messageModel instanceof DistributionListMessageModel) {
  3342. DistributionListService ds = ThreemaApplication.requireServiceManager().getDistributionListService();
  3343. if (ds != null) {
  3344. return ds.createReceiver(ds.getById(((DistributionListMessageModel) messageModel).getDistributionListId()));
  3345. }
  3346. }
  3347. throw new ThreemaException("No receiver for this message");
  3348. }
  3349. /******************************************************************************************************/
  3350. public interface SendResultListener {
  3351. void onError(String errorMessage);
  3352. void onCompleted();
  3353. }
  3354. /**
  3355. * Send media messages of any kind to an arbitrary number of receivers using a thread pool
  3356. *
  3357. * @param mediaItems List of MediaItems to be sent
  3358. * @param messageReceivers List of MessageReceivers
  3359. */
  3360. @AnyThread
  3361. @Override
  3362. public void sendMediaAsync(@NonNull List<MediaItem> mediaItems, @NonNull List<MessageReceiver> messageReceivers) {
  3363. sendMediaAsync(mediaItems, messageReceivers, null);
  3364. }
  3365. /**
  3366. * Send media messages of any kind to an arbitrary number of receivers using a thread pool
  3367. *
  3368. * @param mediaItems List of MediaItems to be sent
  3369. * @param messageReceivers List of MessageReceivers
  3370. * @param sendResultListener Listener to notify when messages are queued
  3371. */
  3372. @AnyThread
  3373. @Override
  3374. public void sendMediaAsync(
  3375. @NonNull final List<MediaItem> mediaItems,
  3376. @NonNull final List<MessageReceiver> messageReceivers,
  3377. @Nullable final SendResultListener sendResultListener
  3378. ) {
  3379. ExecutorServices.getSendMessageExecutorService().submit(() -> {
  3380. sendMedia(mediaItems, messageReceivers, sendResultListener);
  3381. });
  3382. }
  3383. /**
  3384. * Send media messages of any kind to an arbitrary number of receivers in a single thread i.e. one message after the other
  3385. *
  3386. * @param mediaItems List of MediaItems to be sent
  3387. * @param messageReceivers List of MessageReceivers
  3388. */
  3389. @AnyThread
  3390. @Override
  3391. public void sendMediaSingleThread(
  3392. @NonNull final List<MediaItem> mediaItems,
  3393. @NonNull final List<MessageReceiver> messageReceivers) {
  3394. ExecutorServices.getSendMessageSingleThreadExecutorService().submit(() -> {
  3395. sendMedia(mediaItems, messageReceivers, null);
  3396. });
  3397. }
  3398. /**
  3399. * Send media messages of any kind to an arbitrary number of receivers
  3400. *
  3401. * @param mediaItems List of MediaItems to be sent
  3402. * @param messageReceivers List of MessageReceivers
  3403. * @param sendResultListener Listener to notify when messages are queued
  3404. * @return AbstractMessageModel of a successfully queued message, null if no message could be queued
  3405. */
  3406. @WorkerThread
  3407. @Override
  3408. public @Nullable AbstractMessageModel sendMedia(
  3409. @NonNull final List<MediaItem> mediaItems,
  3410. @NonNull final List<MessageReceiver> messageReceivers,
  3411. @Nullable final SendResultListener sendResultListener
  3412. ) {
  3413. AbstractMessageModel successfulMessageModel = null;
  3414. int failedCounter = 0;
  3415. // resolve receivers to account for distribution lists
  3416. final MessageReceiver[] resolvedReceivers = MessageUtil.addDistributionListReceivers(messageReceivers.toArray(new MessageReceiver[0]));
  3417. logger.info("sendMedia: Sending {} items to {} receivers", mediaItems.size(), resolvedReceivers.length);
  3418. String correlationId = getCorrelationId();
  3419. for (MediaItem mediaItem : mediaItems) {
  3420. logger.info("sendMedia: Now sending item of type {}", mediaItem.getType());
  3421. if (TYPE_TEXT == mediaItem.getType()) {
  3422. String text = mediaItem.getCaption();
  3423. if (!TestUtil.isEmptyOrNull(text)) {
  3424. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3425. try {
  3426. successfulMessageModel = sendText(text, messageReceiver);
  3427. if (successfulMessageModel != null) {
  3428. logger.info("Text successfully sent");
  3429. } else {
  3430. failedCounter++;
  3431. logger.info("Text send failed");
  3432. }
  3433. } catch (Exception e) {
  3434. failedCounter++;
  3435. logger.error("Could not send text message", e);
  3436. }
  3437. }
  3438. } else {
  3439. failedCounter++;
  3440. logger.info("Text is empty");
  3441. }
  3442. continue;
  3443. } else if (TYPE_LOCATION == mediaItem.getType()) {
  3444. Location location = GeoLocationUtil.getLocationFromUri(mediaItem.getUri());
  3445. if (location != null) {
  3446. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3447. try {
  3448. successfulMessageModel = sendLocation(location, "", messageReceiver, null);
  3449. } catch (Exception e) {
  3450. failedCounter++;
  3451. logger.error("Could not send location message");
  3452. }
  3453. }
  3454. } else {
  3455. failedCounter++;
  3456. logger.info("Sending location failed: invalid location");
  3457. }
  3458. continue;
  3459. }
  3460. final Map<MessageReceiver, AbstractMessageModel> messageModels = new HashMap<>();
  3461. final FileDataModel fileDataModel = createFileDataModel(context, mediaItem);
  3462. if (fileDataModel == null) {
  3463. logger.info("Unable to create FileDataModel");
  3464. failedCounter++;
  3465. continue;
  3466. }
  3467. if (!createFileMessagesAndSetPending(correlationId, mediaItem, resolvedReceivers, messageModels, fileDataModel)) {
  3468. logger.info("Unable to create messages");
  3469. failedCounter++;
  3470. continue;
  3471. }
  3472. if (!allChatsArePrivate(resolvedReceivers)) {
  3473. saveToGallery(mediaItem);
  3474. }
  3475. try {
  3476. final Map<String, Object> metaData = new HashMap<>();
  3477. final byte[] contentData = generateContentData(mediaItem, resolvedReceivers, messageModels, fileDataModel, metaData);
  3478. final byte[] thumbnailData = generateThumbnailData(mediaItem, fileDataModel, metaData);
  3479. fileDataModel.setMetaData(metaData);
  3480. if (thumbnailData != null) {
  3481. writeThumbnails(messageModels, resolvedReceivers, thumbnailData);
  3482. } else {
  3483. logger.info("Unable to generate thumbnails");
  3484. }
  3485. if (contentData != null) {
  3486. if (encryptAndSend(resolvedReceivers, messageModels, fileDataModel, thumbnailData, contentData)) {
  3487. successfulMessageModel = messageModels.get(resolvedReceivers[0]);
  3488. } else {
  3489. throw new ThreemaException("Error encrypting and sending");
  3490. }
  3491. } else {
  3492. logger.info("Error encrypting and sending");
  3493. failedCounter++;
  3494. markAsTerminallyFailed(resolvedReceivers, messageModels);
  3495. }
  3496. } catch (ThreemaException e) {
  3497. if (e instanceof TranscodeCanceledException) {
  3498. logger.info("Video transcoding canceled");
  3499. // canceling is not really a failure
  3500. } else {
  3501. logger.error("Exception", e);
  3502. failedCounter++;
  3503. }
  3504. markAsTerminallyFailed(resolvedReceivers, messageModels);
  3505. }
  3506. }
  3507. if (failedCounter == 0) {
  3508. logger.info("sendMedia: Successfully queued.");
  3509. if (sendResultListener != null) {
  3510. sendResultListener.onCompleted();
  3511. }
  3512. } else {
  3513. logger.warn("sendMedia: Did not complete successfully, failedCounter={}", failedCounter);
  3514. final String errorString = context.getString(R.string.an_error_occurred_during_send);
  3515. logger.info(errorString);
  3516. RuntimeUtil.runOnUiThread(() -> Toast.makeText(context, errorString, Toast.LENGTH_LONG).show());
  3517. if (sendResultListener != null) {
  3518. sendResultListener.onError(errorString);
  3519. }
  3520. }
  3521. return successfulMessageModel;
  3522. }
  3523. /**
  3524. * Write thumbnails to local storage
  3525. */
  3526. private void writeThumbnails(Map<MessageReceiver, AbstractMessageModel> messageModels, MessageReceiver[] resolvedReceivers, byte[] thumbnailData) {
  3527. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3528. if (thumbnailData != null) {
  3529. try {
  3530. fileService.writeConversationMediaThumbnail(messageModels.get(messageReceiver), thumbnailData);
  3531. fireOnModifiedMessage(messageModels.get(messageReceiver));
  3532. } catch (Exception ignored) {
  3533. // having no thumbnail is not really fatal
  3534. }
  3535. }
  3536. }
  3537. }
  3538. /**
  3539. * Generate content data for this MediaItem
  3540. *
  3541. * @return content data as a byte array or null if content data could not be generated
  3542. */
  3543. @WorkerThread
  3544. private @Nullable byte[] generateContentData(
  3545. @NonNull MediaItem mediaItem,
  3546. @NonNull MessageReceiver[] resolvedReceivers,
  3547. @NonNull Map<MessageReceiver, AbstractMessageModel> messageModels,
  3548. @NonNull FileDataModel fileDataModel,
  3549. @NonNull Map<String, Object> metaData
  3550. ) throws ThreemaException {
  3551. switch (mediaItem.getType()) {
  3552. case TYPE_VIDEO:
  3553. // fallthrough
  3554. case TYPE_VIDEO_CAM:
  3555. @VideoTranscoder.TranscoderResult int result = transcodeVideo(mediaItem, resolvedReceivers, messageModels);
  3556. if (result == VideoTranscoder.SUCCESS) {
  3557. return getContentData(mediaItem);
  3558. } else if (result == VideoTranscoder.CANCELED) {
  3559. throw new TranscodeCanceledException();
  3560. }
  3561. break;
  3562. case TYPE_IMAGE:
  3563. // scale and rotate / flip images
  3564. int maxSize = ConfigUtils.getPreferredImageDimensions(mediaItem.getImageScale() == ImageScale_DEFAULT ?
  3565. preferenceService.getImageScale() : mediaItem.getImageScale());
  3566. Bitmap bitmap = null;
  3567. try {
  3568. boolean hasNoTransparency = MimeUtil.MIME_TYPE_IMAGE_JPEG.equals(mediaItem.getMimeType());
  3569. bitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), maxSize, false, false, false);
  3570. if (bitmap != null) {
  3571. bitmap = adjustBitmapOrientation(bitmap, mediaItem, metaData);
  3572. final byte[] imageByteArray;
  3573. if (hasNoTransparency) {
  3574. imageByteArray = BitmapUtil.getJpegByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip());
  3575. } else {
  3576. imageByteArray = BitmapUtil.getPngByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip());
  3577. if (!MimeUtil.MIME_TYPE_IMAGE_PNG.equals(mediaItem.getMimeType())) {
  3578. fileDataModel.setMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
  3579. if (fileDataModel.getFileName() != null) {
  3580. int dot = fileDataModel.getFileName().lastIndexOf(".");
  3581. if (dot > 1) {
  3582. String filenamePart = fileDataModel.getFileName().substring(0, dot);
  3583. fileDataModel.setFileName(filenamePart + ".png");
  3584. }
  3585. }
  3586. }
  3587. }
  3588. if (imageByteArray != null) {
  3589. fileDataModel.setFileSize(imageByteArray.length);
  3590. ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  3591. outputStream.write(new byte[NaCl.BOXOVERHEAD]);
  3592. outputStream.write(imageByteArray);
  3593. return outputStream.toByteArray();
  3594. }
  3595. }
  3596. } catch (Exception e) {
  3597. logger.error("Exception", e);
  3598. } finally {
  3599. if (bitmap != null && !bitmap.isRecycled()) {
  3600. bitmap.recycle();
  3601. }
  3602. }
  3603. break;
  3604. case TYPE_IMAGE_CAM:
  3605. // cam images will always be sent in their original size. no scaling needed but possibly rotate and flip
  3606. try (InputStream inputStream = getFromUri(context, mediaItem.getUri())) {
  3607. if (inputStream != null && inputStream.available() > 0) {
  3608. bitmap = BitmapFactory.decodeStream(new BufferedInputStream(inputStream), null, null);
  3609. if (bitmap != null) {
  3610. bitmap = adjustBitmapOrientation(bitmap, mediaItem, metaData);
  3611. final byte[] imageByteArray = BitmapUtil.getJpegByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip());
  3612. if (imageByteArray != null) {
  3613. ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  3614. outputStream.write(new byte[NaCl.BOXOVERHEAD]);
  3615. outputStream.write(imageByteArray);
  3616. return outputStream.toByteArray();
  3617. }
  3618. }
  3619. }
  3620. } catch (Exception e) {
  3621. logger.error("Exception", e);
  3622. }
  3623. break;
  3624. case TYPE_IMAGE_ANIMATED:
  3625. metaData.put(FileDataModel.METADATA_KEY_ANIMATED, true);
  3626. // fallthrough
  3627. case TYPE_VOICEMESSAGE:
  3628. // fallthrough
  3629. case TYPE_FILE:
  3630. // "regular" file messages
  3631. return getContentData(mediaItem);
  3632. default:
  3633. // media type currently not supported
  3634. break;
  3635. }
  3636. return null;
  3637. }
  3638. /**
  3639. * Rotate/flip bitmap according to exif information and add final dimensions to the file message's meta data also keeping in
  3640. * account local orientation (if any)
  3641. *
  3642. * @param bitmap The Bitmap
  3643. * @param mediaItem The MediaItem instance that contains orientation info about this particular item
  3644. * @param metaData A map with meta data that is going to be added to a file message
  3645. * @return A new bitmap with adjusted orientation
  3646. */
  3647. @NonNull
  3648. private Bitmap adjustBitmapOrientation(
  3649. @NonNull Bitmap bitmap,
  3650. @NonNull MediaItem mediaItem,
  3651. @NonNull Map<String, Object> metaData
  3652. ) {
  3653. bitmap = BitmapUtil.rotateBitmap(
  3654. bitmap,
  3655. mediaItem.getExifRotation(),
  3656. mediaItem.getExifFlip());
  3657. boolean isRotated = mediaItem.getRotation() == 90 || mediaItem.getRotation() == 270;
  3658. metaData.put(FileDataModel.METADATA_KEY_WIDTH, isRotated ? bitmap.getHeight() : bitmap.getWidth());
  3659. metaData.put(FileDataModel.METADATA_KEY_HEIGHT, isRotated ? bitmap.getWidth() : bitmap.getHeight());
  3660. return bitmap;
  3661. }
  3662. /**
  3663. * Generate thumbnail data for this MediaItem
  3664. *
  3665. * @return byte array of the thumbnail bitmap, null if thumbnail could not be generated
  3666. */
  3667. @WorkerThread
  3668. private @Nullable byte[] generateThumbnailData(
  3669. @NonNull MediaItem mediaItem,
  3670. @NonNull FileDataModel fileDataModel,
  3671. @NonNull Map<String, Object> metaData
  3672. ) {
  3673. Bitmap thumbnailBitmap = null;
  3674. int mediaType = mediaItem.getType();
  3675. // we want thumbnails for images and videos even if they are to be sent as files
  3676. if (MimeUtil.isSupportedImageFile(fileDataModel.getMimeType())) {
  3677. mediaType = TYPE_IMAGE;
  3678. } else if (MimeUtil.isVideoFile(fileDataModel.getMimeType())) {
  3679. mediaType = TYPE_VIDEO;
  3680. }
  3681. switch (mediaType) {
  3682. case MediaItem.TYPE_VIDEO:
  3683. // fallthrough
  3684. case MediaItem.TYPE_VIDEO_CAM:
  3685. // add duration to metadata
  3686. long trimmedDuration = mediaItem.getDurationMs();
  3687. if (mediaItem.getEndTimeMs() != TIME_UNDEFINED && (mediaItem.getEndTimeMs() != 0L || mediaItem.getStartTimeMs() != 0L)) {
  3688. trimmedDuration = mediaItem.getEndTimeMs() - mediaItem.getStartTimeMs();
  3689. } else {
  3690. if (mediaItem.getDurationMs() == 0) {
  3691. // empty duration means full video
  3692. trimmedDuration = VideoUtil.getVideoDuration(context, mediaItem.getUri());
  3693. mediaItem.setDurationMs(trimmedDuration);
  3694. }
  3695. }
  3696. metaData.put(FileDataModel.METADATA_KEY_DURATION, (float) trimmedDuration / (float) DateUtils.SECOND_IN_MILLIS);
  3697. thumbnailBitmap = IconUtil.getVideoThumbnailFromUri(context, mediaItem);
  3698. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPEG);
  3699. break;
  3700. case MediaItem.TYPE_IMAGE:
  3701. BitmapUtil.ExifOrientation exifOrientation = BitmapUtil.getExifOrientation(context, mediaItem.getUri());
  3702. mediaItem.setExifRotation((int) exifOrientation.getRotation());
  3703. mediaItem.setExifFlip(exifOrientation.getFlip());
  3704. boolean hasNoTransparency = MimeUtil.MIME_TYPE_IMAGE_JPEG.equals(mediaItem.getMimeType());
  3705. if (hasNoTransparency && mediaItem.getRenderingType() != RENDERING_STICKER) {
  3706. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPEG);
  3707. } else {
  3708. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
  3709. }
  3710. thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, false, true, false);
  3711. if (thumbnailBitmap != null) {
  3712. thumbnailBitmap = BitmapUtil.rotateBitmap(BitmapUtil.rotateBitmap(
  3713. thumbnailBitmap,
  3714. mediaItem.getExifRotation(),
  3715. mediaItem.getExifFlip()), mediaItem.getRotation(), mediaItem.getFlip());
  3716. }
  3717. break;
  3718. case MediaItem.TYPE_IMAGE_CAM:
  3719. // camera images are always sent as JPGs
  3720. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPEG);
  3721. thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, false, true, false);
  3722. if (thumbnailBitmap != null) {
  3723. thumbnailBitmap = BitmapUtil.rotateBitmap(BitmapUtil.rotateBitmap(
  3724. thumbnailBitmap,
  3725. mediaItem.getExifRotation(),
  3726. mediaItem.getExifFlip()), mediaItem.getRotation(), mediaItem.getFlip());
  3727. }
  3728. break;
  3729. case TYPE_IMAGE_ANIMATED:
  3730. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
  3731. thumbnailBitmap = IconUtil.getThumbnailFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, fileDataModel.getMimeType(), true);
  3732. break;
  3733. case MediaItem.TYPE_VOICEMESSAGE:
  3734. metaData.put(FileDataModel.METADATA_KEY_DURATION, (float) mediaItem.getDurationMs() / (float) DateUtils.SECOND_IN_MILLIS);
  3735. // voice messages do not have thumbnails
  3736. thumbnailBitmap = null;
  3737. break;
  3738. case MediaItem.TYPE_FILE:
  3739. // just an arbitrary file
  3740. thumbnailBitmap = null;
  3741. break;
  3742. default:
  3743. break;
  3744. }
  3745. final byte[] thumbnailData;
  3746. if (thumbnailBitmap != null) {
  3747. // convert bitmap to byte array
  3748. if (MimeUtil.MIME_TYPE_IMAGE_JPEG.equals(fileDataModel.getThumbnailMimeType())) {
  3749. thumbnailData = BitmapUtil.bitmapToJpegByteArray(thumbnailBitmap);
  3750. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPEG);
  3751. } else {
  3752. thumbnailData = BitmapUtil.bitmapToPngByteArray(thumbnailBitmap);
  3753. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
  3754. }
  3755. thumbnailBitmap.recycle();
  3756. } else {
  3757. thumbnailData = null;
  3758. }
  3759. return thumbnailData;
  3760. }
  3761. /**
  3762. * Encrypt content and thumbnail data, upload blobs and queue messages for the specified MediaItem
  3763. *
  3764. * @param resolvedReceivers MessageReceivers to send the MediaItem to
  3765. * @param messageModels MessageModels for above MessageReceivers
  3766. * @param fileDataModel fileDataModel for this message
  3767. * @param thumbnailData Byte Array of thumbnail bitmap to be uploaded as a blob
  3768. * @param contentData Byte Array of Content to be uploaded as a blob
  3769. * @return true if the message was queued successfully, false otherwise. Note that errors that occur during sending are not handled here.
  3770. */
  3771. @WorkerThread
  3772. private boolean encryptAndSend(
  3773. @NonNull MessageReceiver<AbstractMessageModel>[] resolvedReceivers,
  3774. @NonNull Map<MessageReceiver, AbstractMessageModel> messageModels,
  3775. @NonNull FileDataModel fileDataModel,
  3776. @Nullable byte[] thumbnailData,
  3777. @NonNull byte[] contentData
  3778. ) {
  3779. final SymmetricEncryptionResult[] contentEncryptResult = new SymmetricEncryptionResult[1];
  3780. final SymmetricEncryptionResult[] thumbnailEncryptResult = new SymmetricEncryptionResult[1];
  3781. thumbnailEncryptResult[0] = null;
  3782. contentEncryptResult[0] = null;
  3783. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3784. // save content first as it will be modified later on
  3785. AbstractMessageModel messageModel = messageModels.get(messageReceiver);
  3786. if (messageModel == null) {
  3787. // no messagemodel has been created for this receiver - skip
  3788. continue;
  3789. }
  3790. if (messageReceiver instanceof GroupMessageReceiver
  3791. && groupService.isNotesGroup(((GroupMessageReceiver) messageReceiver).getGroup())
  3792. ) {
  3793. // In case of a notes group, we set the message state directly to read
  3794. messageModel.setState(MessageState.READ);
  3795. } else {
  3796. // Otherwise we initialize the message model with pending to show a progress bar
  3797. messageModel.setState(MessageState.PENDING); // shows a progress bar
  3798. }
  3799. save(messageModel);
  3800. try {
  3801. fileService.writeConversationMedia(messageModel, contentData, NaCl.BOXOVERHEAD, contentData.length - NaCl.BOXOVERHEAD);
  3802. } catch (Exception e) {
  3803. // Failure to write local media is not necessarily fatal, continue
  3804. logger.debug("Exception", e);
  3805. }
  3806. }
  3807. for (MessageReceiver<AbstractMessageModel> messageReceiver : resolvedReceivers) {
  3808. //enqueue processing and uploading stuff...
  3809. AbstractMessageModel messageModel = messageModels.get(messageReceiver);
  3810. if (messageModel == null) {
  3811. // no messagemodel has been created for this receiver - skip
  3812. logger.info("Mo MessageModel could be created for this receiver - skip");
  3813. continue;
  3814. }
  3815. messageSendingService.addToQueue(new MessageSendingService.MessageSendingProcess() {
  3816. private byte[] thumbnailBlobId;
  3817. private byte[] contentBlobId;
  3818. public boolean success = false;
  3819. @Override
  3820. public MessageReceiver<AbstractMessageModel> getReceiver() {
  3821. return messageReceiver;
  3822. }
  3823. @Override
  3824. public AbstractMessageModel getMessageModel() {
  3825. return messageModel;
  3826. }
  3827. @Override
  3828. public boolean send() throws Exception {
  3829. SendMachine sendMachine = getSendMachine(messageModel);
  3830. sendMachine.reset()
  3831. .next(() -> {
  3832. if (getReceiver().sendMediaData()) {
  3833. // encrypt file data
  3834. // note that encryptFileData() will overwrite contents of provided content data!
  3835. if (contentEncryptResult[0] == null) {
  3836. contentEncryptResult[0] = symmetricEncryptionService.encryptInplace(contentData, ProtocolDefines.FILE_NONCE);
  3837. if (contentEncryptResult[0].isEmpty()) {
  3838. throw new ThreemaException("File data encrypt failed");
  3839. }
  3840. }
  3841. messageModel.setState(MessageState.UPLOADING);
  3842. save(messageModel);
  3843. }
  3844. fileDataModel.setFileSize(contentData.length - NaCl.BOXOVERHEAD);
  3845. messageModel.setFileData(fileDataModel);
  3846. fireOnModifiedMessage(messageModel);
  3847. })
  3848. .next(() -> {
  3849. if (getReceiver().sendMediaData()) {
  3850. // upload file data
  3851. BlobUploader blobUploader = initUploader(
  3852. getMessageModel(),
  3853. contentEncryptResult[0].getData(),
  3854. getReceiver()
  3855. );
  3856. blobUploader.progressListener = new ProgressListener() {
  3857. @Override
  3858. public void updateProgress(int progress) {
  3859. updateMessageLoadingProgress(messageModel, progress);
  3860. }
  3861. @Override
  3862. public void onFinished(boolean success) {
  3863. setMessageLoadingFinished(messageModel);
  3864. }
  3865. };
  3866. contentBlobId = blobUploader.upload();
  3867. }
  3868. })
  3869. .next(() -> {
  3870. if (getReceiver().sendMediaData()) {
  3871. // encrypt and upload thumbnail
  3872. if (thumbnailData != null) {
  3873. thumbnailEncryptResult[0] = symmetricEncryptionService
  3874. .encrypt(thumbnailData, contentEncryptResult[0].getKey(), ProtocolDefines.FILE_THUMBNAIL_NONCE);
  3875. if (thumbnailEncryptResult[0].isEmpty()) {
  3876. throw new ThreemaException("Thumbnail encrypt failed");
  3877. } else {
  3878. BlobUploader blobUploader = initUploader(
  3879. getMessageModel(),
  3880. thumbnailEncryptResult[0].getData(),
  3881. getReceiver()
  3882. );
  3883. blobUploader.progressListener = new ProgressListener() {
  3884. @Override
  3885. public void updateProgress(int progress) {
  3886. updateMessageLoadingProgress(messageModel, progress);
  3887. }
  3888. @Override
  3889. public void onFinished(boolean success) {
  3890. setMessageLoadingFinished(messageModel);
  3891. }
  3892. };
  3893. thumbnailBlobId = blobUploader.upload();
  3894. fireOnModifiedMessage(messageModel);
  3895. }
  3896. }
  3897. }
  3898. })
  3899. .next(() -> {
  3900. getReceiver().createAndSendFileMessage(
  3901. thumbnailBlobId,
  3902. contentBlobId,
  3903. contentEncryptResult[0],
  3904. messageModel,
  3905. null,
  3906. null
  3907. );
  3908. updateOutgoingMessageState(messageModel,
  3909. getReceiver().sendMediaData() && getReceiver().offerRetry() ?
  3910. MessageState.SENDING :
  3911. MessageState.SENT, new Date());
  3912. messageModel.setFileData(fileDataModel);
  3913. // save updated model
  3914. save(messageModel);
  3915. })
  3916. .next(() -> {
  3917. messageModel.setSaved(true);
  3918. // Verify current saved state
  3919. updateOutgoingMessageState(messageModel,
  3920. getReceiver().sendMediaData() && getReceiver().offerRetry() ?
  3921. MessageState.SENDING :
  3922. MessageState.SENT, new Date());
  3923. if (!getReceiver().sendMediaData()) {
  3924. // update status for message that stay local
  3925. fireOnModifiedMessage(messageModel);
  3926. }
  3927. success = true;
  3928. });
  3929. if (success) {
  3930. removeSendMachine(sendMachine);
  3931. }
  3932. return success;
  3933. }
  3934. });
  3935. }
  3936. return true;
  3937. }
  3938. /**
  3939. * Create MessageModels for all receivers, save local thumbnail and set MessageModels to PENDING for instant UI feedback
  3940. *
  3941. * @return true if all was hunky dory, false if an error occurred
  3942. */
  3943. @WorkerThread
  3944. private boolean createFileMessagesAndSetPending(
  3945. String correlationId,
  3946. MediaItem mediaItem,
  3947. MessageReceiver[] resolvedReceivers,
  3948. Map<MessageReceiver, AbstractMessageModel> messageModels,
  3949. FileDataModel fileDataModel
  3950. ) {
  3951. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3952. final AbstractMessageModel messageModel = messageReceiver.createLocalModel(MessageType.FILE, MimeUtil.getContentTypeFromFileData(fileDataModel), new Date());
  3953. cache(messageModel);
  3954. messageModel.setOutbox(true);
  3955. messageModel.setState(MessageState.PENDING); // shows a progress bar
  3956. messageModel.setFileData(fileDataModel);
  3957. messageModel.setCorrelationId(correlationId);
  3958. String trimmedCaption = mediaItem.getTrimmedCaption();
  3959. if (trimmedCaption != null && !trimmedCaption.isBlank()) {
  3960. messageModel.setCaption(trimmedCaption);
  3961. }
  3962. messageModel.setSaved(true);
  3963. messageReceiver.saveLocalModel(messageModel);
  3964. messageReceiver.bumpLastUpdate();
  3965. messageModels.put(messageReceiver, messageModel);
  3966. fireOnCreatedMessage(messageModel);
  3967. }
  3968. return true;
  3969. }
  3970. @SuppressLint("Range")
  3971. public @Nullable FileDataModel createFileDataModel(Context context, MediaItem mediaItem) {
  3972. ContentResolver contentResolver = context.getContentResolver();
  3973. String mimeType = mediaItem.getMimeType();
  3974. String filename = mediaItem.getFilename();
  3975. if (mediaItem.getUri() == null) {
  3976. return null;
  3977. }
  3978. if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(mediaItem.getUri().getScheme())) {
  3979. if (TestUtil.isEmptyOrNull(filename)) {
  3980. File file = new File(mediaItem.getUri().getPath());
  3981. filename = file.getName();
  3982. }
  3983. } else {
  3984. if (TestUtil.isEmptyOrNull(filename) || TestUtil.isEmptyOrNull(mimeType)) {
  3985. String[] proj = {
  3986. DocumentsContract.Document.COLUMN_DISPLAY_NAME,
  3987. DocumentsContract.Document.COLUMN_MIME_TYPE
  3988. };
  3989. try (Cursor cursor = contentResolver.query(mediaItem.getUri(), proj, null, null, null)) {
  3990. if (cursor != null && cursor.moveToFirst()) {
  3991. if (TestUtil.isEmptyOrNull(filename)) {
  3992. filename = cursor.getString(
  3993. cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
  3994. }
  3995. if (TestUtil.isEmptyOrNull(mimeType) || MimeUtil.MIME_TYPE_DEFAULT.equals(mimeType)) {
  3996. mimeType = cursor.getString(
  3997. cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE));
  3998. }
  3999. }
  4000. } catch (Exception e) {
  4001. logger.error("Unable to query content provider", e);
  4002. }
  4003. }
  4004. }
  4005. if (TestUtil.isEmptyOrNull(mimeType) || MimeUtil.MIME_TYPE_DEFAULT.equals(mimeType)) {
  4006. mimeType = FileUtil.getMimeTypeFromUri(context, mediaItem.getUri());
  4007. }
  4008. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
  4009. // non-animated images are being sent as png files
  4010. // we should fix the mime type before creating a local message model in order not to confuse the chat adapter
  4011. if (MimeUtil.isAnimatedImageFormat(mimeType)
  4012. && mediaItem.getType() != TYPE_IMAGE_ANIMATED
  4013. && mediaItem.getType() != TYPE_FILE
  4014. && mediaItem.getImageScale() != PreferenceService.ImageScale_SEND_AS_FILE) {
  4015. mimeType = MimeUtil.MIME_TYPE_IMAGE_PNG;
  4016. }
  4017. }
  4018. @FileData.RenderingType int renderingType = mediaItem.getRenderingType();
  4019. // rendering type overrides
  4020. switch (mediaItem.getType()) {
  4021. case TYPE_VOICEMESSAGE:
  4022. filename = FileUtil.getDefaultFilename(mimeType); // the internal temporary file name is of no use to the recipient
  4023. renderingType = FileData.RENDERING_MEDIA;
  4024. break;
  4025. case TYPE_IMAGE_ANIMATED:
  4026. if (renderingType == FileData.RENDERING_DEFAULT) {
  4027. // do not override stickers
  4028. renderingType = FileData.RENDERING_MEDIA;
  4029. }
  4030. break;
  4031. case TYPE_FILE:
  4032. // "regular" file messages
  4033. renderingType = FileData.RENDERING_DEFAULT;
  4034. break;
  4035. case TYPE_VIDEO:
  4036. if (renderingType == FileData.RENDERING_MEDIA) {
  4037. // videos in formats other than MP4 are always transcoded and result in an MP4 file
  4038. mimeType = MimeUtil.MIME_TYPE_VIDEO_MP4;
  4039. }
  4040. // fallthrough
  4041. default:
  4042. if (mediaItem.getImageScale() == PreferenceService.ImageScale_SEND_AS_FILE || mediaItem.getVideoSize() == PreferenceService.VideoSize_SEND_AS_FILE) {
  4043. // images with scale type "send as file" get the default rendering type and a file name
  4044. renderingType = FileData.RENDERING_DEFAULT;
  4045. mediaItem.setType(TYPE_FILE);
  4046. } else {
  4047. // unlike with "real" files we override the filename for regular (RENDERING_MEDIA) images and videos with a generic one to prevent privacy leaks
  4048. // this mimics the behavior of traditional image messages that did not have a filename at all
  4049. filename = FileUtil.getDefaultFilename(mimeType);
  4050. }
  4051. break;
  4052. }
  4053. if (TestUtil.isEmptyOrNull(filename)) {
  4054. filename = FileUtil.getDefaultFilename(mimeType);
  4055. }
  4056. String caption = mediaItem.getTrimmedCaption();
  4057. if (caption != null && caption.isBlank()) {
  4058. caption = null;
  4059. }
  4060. return new FileDataModel(mimeType,
  4061. null,
  4062. 0,
  4063. filename,
  4064. renderingType,
  4065. caption,
  4066. true,
  4067. null);
  4068. }
  4069. /**
  4070. * Transcode and trim this video according to the parameters set in the MediaItem object
  4071. *
  4072. * @return Result of transcoding
  4073. */
  4074. @WorkerThread
  4075. private @VideoTranscoder.TranscoderResult int transcodeVideo(MediaItem mediaItem, MessageReceiver[] resolvedReceivers, Map<MessageReceiver, AbstractMessageModel> messageModels) {
  4076. final MessagePlayerService messagePlayerService;
  4077. try {
  4078. messagePlayerService = ThreemaApplication.requireServiceManager().getMessagePlayerService();
  4079. } catch (ThreemaException e) {
  4080. logger.error("Exception", e);
  4081. return VideoTranscoder.FAILURE;
  4082. }
  4083. int targetBitrate;
  4084. @PreferenceService.VideoSize int desiredVideoSize = preferenceService.getVideoSize();
  4085. if (mediaItem.getVideoSize() != PreferenceService.VideoSize_DEFAULT) {
  4086. desiredVideoSize = mediaItem.getVideoSize();
  4087. }
  4088. try {
  4089. targetBitrate = VideoConfig.getTargetVideoBitrate(context, mediaItem, desiredVideoSize);
  4090. } catch (ThreemaException e) {
  4091. logger.error("Error getting target bitrate", e);
  4092. // skip this MediaItem
  4093. markAsTerminallyFailed(resolvedReceivers, messageModels);
  4094. return VideoTranscoder.FAILURE;
  4095. }
  4096. if (targetBitrate == -1) {
  4097. // will not fit
  4098. logger.info("Video file ist too large");
  4099. RuntimeUtil.runOnUiThread(() -> Toast.makeText(context, context.getString(R.string.file_too_large, MAX_BLOB_SIZE_MB), Toast.LENGTH_SHORT).show());
  4100. // skip this MediaItem
  4101. markAsTerminallyFailed(resolvedReceivers, messageModels);
  4102. return VideoTranscoder.FAILURE;
  4103. }
  4104. logger.info("Target bitrate = {}", targetBitrate);
  4105. if (mediaItem.hasChanges() ||
  4106. targetBitrate > 0 ||
  4107. !MimeUtil.MIME_TYPE_VIDEO_MP4.equalsIgnoreCase(mediaItem.getMimeType())) {
  4108. logger.info("Video needs transcoding");
  4109. // set models to TRANSCODING state
  4110. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  4111. AbstractMessageModel messageModel = entry.getValue();
  4112. messageModel.setState(MessageState.TRANSCODING);
  4113. save(messageModel);
  4114. fireOnModifiedMessage(messageModel);
  4115. }
  4116. File outputFile;
  4117. try {
  4118. outputFile = fileService.createTempFile(".trans", ".mp4", false);
  4119. } catch (IOException e) {
  4120. logger.error("Unable to open temp file");
  4121. // skip this MediaItem
  4122. markAsTerminallyFailed(resolvedReceivers, messageModels);
  4123. return VideoTranscoder.FAILURE;
  4124. }
  4125. final VideoTranscoder.Builder transcoderBuilder = new VideoTranscoder.Builder(mediaItem.getUri(), outputFile);
  4126. transcoderBuilder.includeAudio(!mediaItem.isMuted());
  4127. if (mediaItem.needsTrimming()) {
  4128. transcoderBuilder.trim(mediaItem.getStartTimeMs(), mediaItem.getEndTimeMs());
  4129. }
  4130. if (targetBitrate > 0) {
  4131. int maxSize = VideoConfig.getMaxSizeFromBitrate(targetBitrate);
  4132. transcoderBuilder.maxFrameHeight(maxSize);
  4133. transcoderBuilder.maxFrameWidth(maxSize);
  4134. transcoderBuilder.videoBitRate(targetBitrate);
  4135. transcoderBuilder.iFrameInterval(2);
  4136. transcoderBuilder.frameRate(25);
  4137. }
  4138. final VideoTranscoder videoTranscoder = transcoderBuilder.build(context);
  4139. synchronized (videoTranscoders) {
  4140. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  4141. AbstractMessageModel messageModel = entry.getValue();
  4142. String key = cancelTranscoding(messageModel);
  4143. videoTranscoders.put(key, new WeakReference<>(videoTranscoder));
  4144. }
  4145. }
  4146. final @VideoTranscoder.TranscoderResult int transcoderResult = videoTranscoder.startSync(new VideoTranscoder.Listener() {
  4147. @Override
  4148. public void onStart() {
  4149. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  4150. AbstractMessageModel messageModel = entry.getValue();
  4151. messagePlayerService.setTranscodeStart(messageModel);
  4152. }
  4153. }
  4154. @Override
  4155. public void onProgress(int progress) {
  4156. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  4157. AbstractMessageModel messageModel = entry.getValue();
  4158. messagePlayerService.setTranscodeProgress(messageModel, progress);
  4159. }
  4160. }
  4161. @Override
  4162. public void onCanceled() {
  4163. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  4164. AbstractMessageModel messageModel = entry.getValue();
  4165. messagePlayerService.setTranscodeFinished(messageModel, true, null);
  4166. }
  4167. }
  4168. @Override
  4169. public void onSuccess(VideoTranscoder.Stats stats) {
  4170. if (stats != null) {
  4171. logger.debug(stats.toString());
  4172. }
  4173. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  4174. AbstractMessageModel messageModel = entry.getValue();
  4175. messagePlayerService.setTranscodeFinished(messageModel, true, null);
  4176. }
  4177. }
  4178. @Override
  4179. public void onFailure() {
  4180. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  4181. AbstractMessageModel messageModel = entry.getValue();
  4182. messagePlayerService.setTranscodeFinished(messageModel, false, "Failure");
  4183. }
  4184. }
  4185. });
  4186. if (transcoderResult != VideoTranscoder.SUCCESS) {
  4187. // failure
  4188. logger.info("Transcoding failure");
  4189. return transcoderResult;
  4190. }
  4191. if (videoTranscoder.hasAudioTranscodingError()) {
  4192. final int errorMessageResource;
  4193. if (videoTranscoder.audioFormatUnsupported()) {
  4194. errorMessageResource = R.string.transcoder_unsupported_audio_format;
  4195. } else {
  4196. errorMessageResource = R.string.transcoder_unknown_audio_error;
  4197. }
  4198. RuntimeUtil.runOnUiThread(() -> Toast.makeText(
  4199. ThreemaApplication.getAppContext(),
  4200. context.getString(errorMessageResource),
  4201. Toast.LENGTH_LONG
  4202. ).show());
  4203. }
  4204. // remove original file and set transcoded file as new source file
  4205. deleteTemporaryFile(mediaItem);
  4206. mediaItem.setUri(Uri.fromFile(outputFile));
  4207. mediaItem.setMimeType(MimeUtil.MIME_TYPE_VIDEO_MP4);
  4208. } else {
  4209. logger.info("No transcoding necessary");
  4210. }
  4211. return VideoTranscoder.SUCCESS;
  4212. }
  4213. /**
  4214. * Generate a random correlation ID that identifies all media sent in one batch
  4215. *
  4216. * @return correlation Id
  4217. */
  4218. @Override
  4219. public String getCorrelationId() {
  4220. final byte[] random = new byte[16];
  4221. new SecureRandom().nextBytes(random);
  4222. return Utils.byteArrayToHexString(random);
  4223. }
  4224. @WorkerThread
  4225. private void deleteTemporaryFile(MediaItem mediaItem) {
  4226. if (mediaItem.getDeleteAfterUse()) {
  4227. if (mediaItem.getUri() != null && ContentResolver.SCHEME_FILE.equalsIgnoreCase(mediaItem.getUri().getScheme())) {
  4228. if (mediaItem.getUri().getPath() != null) {
  4229. FileUtil.deleteFileOrWarn(mediaItem.getUri().getPath(), null, logger);
  4230. }
  4231. }
  4232. }
  4233. }
  4234. /**
  4235. * Check if all chats in the supplied list of MessageReceivers are set to "hidden"
  4236. *
  4237. * @return true if all chats are hidden (i.e. marked as "private"), false if there is at least one chat that is always visible
  4238. */
  4239. private boolean allChatsArePrivate(MessageReceiver[] messageReceivers) {
  4240. for (MessageReceiver messageReceiver : messageReceivers) {
  4241. if (!conversationCategoryService.isPrivateChat(messageReceiver.getUniqueIdString())) {
  4242. return false;
  4243. }
  4244. }
  4245. return true;
  4246. }
  4247. /**
  4248. * Delete message models for specified receivers
  4249. */
  4250. private void markAsTerminallyFailed(
  4251. MessageReceiver<AbstractMessageModel>[] resolvedReceivers,
  4252. Map<MessageReceiver, AbstractMessageModel> messageModels
  4253. ) {
  4254. for (MessageReceiver messageReceiver : resolvedReceivers) {
  4255. remove(messageModels.get(messageReceiver));
  4256. }
  4257. }
  4258. /**
  4259. * Get a byte array for the media represented by the MediaItem leaving room for NaCl Box header
  4260. *
  4261. * @param mediaItem MediaItem containing the Uri of the media
  4262. * @return byte array of the media data or null if error occured
  4263. */
  4264. @WorkerThread
  4265. private byte[] getContentData(MediaItem mediaItem) {
  4266. try (InputStream inputStream = getFromUri(context, mediaItem.getUri())) {
  4267. if (inputStream != null) {
  4268. int fileLength = inputStream.available();
  4269. if (fileLength > MAX_BLOB_SIZE) {
  4270. String errorMessage = context.getString(R.string.file_too_large, MAX_BLOB_SIZE_MB);
  4271. logger.info(errorMessage);
  4272. RuntimeUtil.runOnUiThread(() -> Toast.makeText(ThreemaApplication.getAppContext(), errorMessage, Toast.LENGTH_LONG).show());
  4273. return null;
  4274. }
  4275. if (fileLength == 0) {
  4276. // InputStream may not provide size
  4277. fileLength = MAX_BLOB_SIZE + 1;
  4278. }
  4279. if (ConfigUtils.checkAvailableMemory(fileLength + NaCl.BOXOVERHEAD)) {
  4280. byte[] fileData = new byte[fileLength + NaCl.BOXOVERHEAD];
  4281. try {
  4282. int readCount = 0;
  4283. try {
  4284. readCount = IOUtils.read(inputStream, fileData, NaCl.BOXOVERHEAD, fileLength);
  4285. } catch (Exception e) {
  4286. // it's OK to get an EOF
  4287. }
  4288. if (readCount > MAX_BLOB_SIZE) {
  4289. String errorMessage = context.getString(R.string.file_too_large, MAX_BLOB_SIZE_MB);
  4290. logger.info(errorMessage);
  4291. RuntimeUtil.runOnUiThread(() -> Toast.makeText(ThreemaApplication.getAppContext(), errorMessage, Toast.LENGTH_LONG).show());
  4292. return null;
  4293. }
  4294. if (readCount < fileLength) {
  4295. return Arrays.copyOf(fileData, readCount + NaCl.BOXOVERHEAD);
  4296. }
  4297. return fileData;
  4298. } catch (OutOfMemoryError e) {
  4299. logger.error("Unable to create byte array", e);
  4300. }
  4301. } else {
  4302. logger.info("Not enough memory to create byte array.");
  4303. }
  4304. } else {
  4305. logger.info("Not enough memory to create byte array.");
  4306. }
  4307. } catch (IOException e) {
  4308. logger.error("Unable to open file to send", e);
  4309. }
  4310. return null;
  4311. }
  4312. /**
  4313. * Save outgoing media item recorded from within the app to gallery if enabled
  4314. */
  4315. @WorkerThread
  4316. private void saveToGallery(MediaItem item) {
  4317. if (item.getType() == MediaItem.TYPE_IMAGE_CAM || item.getType() == MediaItem.TYPE_VIDEO_CAM) {
  4318. if (preferenceService.isSaveMedia()) {
  4319. try {
  4320. AbstractMessageModel messageModel = new MessageModel();
  4321. messageModel.setType(item.getType() == TYPE_VIDEO_CAM ? MessageType.VIDEO : MessageType.IMAGE);
  4322. messageModel.setCreatedAt(new Date());
  4323. messageModel.setId(0);
  4324. fileService.copyDecryptedFileIntoGallery(item.getUri(), messageModel);
  4325. } catch (Exception e) {
  4326. logger.error("Exception", e);
  4327. }
  4328. }
  4329. }
  4330. }
  4331. /**
  4332. * @param message the text message user input
  4333. * @return trimmed message
  4334. */
  4335. private String validateTextMessage(@NonNull String message) throws ThreemaException {
  4336. // Strip leading/trailing whitespace and throw if nothing is left
  4337. String trimmedMessage = message.trim();
  4338. if (trimmedMessage.isEmpty()) {
  4339. throw new ThreemaException("Tried to send empty message");
  4340. }
  4341. // Check maximum length in UTF-8 bytes (can be reached quickly with Unicode emojis etc.)
  4342. if (message.getBytes(StandardCharsets.UTF_8).length > ProtocolDefines.MAX_TEXT_MESSAGE_LEN) {
  4343. throw new MessageTooLongException();
  4344. }
  4345. return trimmedMessage;
  4346. }
  4347. }