MessageServiceImpl.java 146 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312
  1. /* _____ _
  2. * |_ _| |_ _ _ ___ ___ _ __ __ _
  3. * | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. * |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. *
  6. * Threema for Android
  7. * Copyright (c) 2013-2021 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.app.Activity;
  23. import android.content.ActivityNotFoundException;
  24. import android.content.ContentResolver;
  25. import android.content.Context;
  26. import android.content.Intent;
  27. import android.database.Cursor;
  28. import android.graphics.Bitmap;
  29. import android.graphics.BitmapFactory;
  30. import android.location.Location;
  31. import android.net.ConnectivityManager;
  32. import android.net.NetworkInfo;
  33. import android.net.Uri;
  34. import android.provider.DocumentsContract;
  35. import android.text.format.DateUtils;
  36. import android.util.SparseIntArray;
  37. import android.widget.Toast;
  38. import com.google.android.gms.common.util.ArrayUtils;
  39. import com.neilalexander.jnacl.NaCl;
  40. import org.apache.commons.io.IOUtils;
  41. import org.slf4j.Logger;
  42. import org.slf4j.LoggerFactory;
  43. import java.io.BufferedInputStream;
  44. import java.io.ByteArrayInputStream;
  45. import java.io.ByteArrayOutputStream;
  46. import java.io.File;
  47. import java.io.FileInputStream;
  48. import java.io.FileOutputStream;
  49. import java.io.IOException;
  50. import java.io.InputStream;
  51. import java.lang.ref.WeakReference;
  52. import java.nio.charset.StandardCharsets;
  53. import java.security.SecureRandom;
  54. import java.sql.SQLException;
  55. import java.util.ArrayList;
  56. import java.util.Collection;
  57. import java.util.Collections;
  58. import java.util.Date;
  59. import java.util.HashMap;
  60. import java.util.HashSet;
  61. import java.util.Iterator;
  62. import java.util.List;
  63. import java.util.Map;
  64. import java.util.Set;
  65. import java.util.concurrent.CopyOnWriteArrayList;
  66. import javax.crypto.CipherInputStream;
  67. import javax.crypto.CipherOutputStream;
  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.R;
  75. import ch.threema.app.ThreemaApplication;
  76. import ch.threema.app.collections.Functional;
  77. import ch.threema.app.collections.IPredicateNonNull;
  78. import ch.threema.app.exceptions.NotAllowedException;
  79. import ch.threema.app.exceptions.TranscodeCanceledException;
  80. import ch.threema.app.listeners.MessageListener;
  81. import ch.threema.app.listeners.ServerMessageListener;
  82. import ch.threema.app.managers.ListenerManager;
  83. import ch.threema.app.messagereceiver.ContactMessageReceiver;
  84. import ch.threema.app.messagereceiver.GroupMessageReceiver;
  85. import ch.threema.app.messagereceiver.MessageReceiver;
  86. import ch.threema.app.processors.MessageAckProcessor;
  87. import ch.threema.app.routines.ReadMessagesRoutine;
  88. import ch.threema.app.services.ballot.BallotService;
  89. import ch.threema.app.services.ballot.BallotUpdateResult;
  90. import ch.threema.app.services.messageplayer.MessagePlayerService;
  91. import ch.threema.app.stores.IdentityStore;
  92. import ch.threema.app.ui.MediaItem;
  93. import ch.threema.app.utils.BallotUtil;
  94. import ch.threema.app.utils.BitmapUtil;
  95. import ch.threema.app.utils.ConfigUtils;
  96. import ch.threema.app.utils.ContactUtil;
  97. import ch.threema.app.utils.ExifInterface;
  98. import ch.threema.app.utils.FileUtil;
  99. import ch.threema.app.utils.GeoLocationUtil;
  100. import ch.threema.app.utils.IconUtil;
  101. import ch.threema.app.utils.MessageUtil;
  102. import ch.threema.app.utils.MimeUtil;
  103. import ch.threema.app.utils.NameUtil;
  104. import ch.threema.app.utils.QuoteUtil;
  105. import ch.threema.app.utils.RuntimeUtil;
  106. import ch.threema.app.utils.StreamUtil;
  107. import ch.threema.app.utils.StringConversionUtil;
  108. import ch.threema.app.utils.TestUtil;
  109. import ch.threema.app.utils.VideoUtil;
  110. import ch.threema.app.video.VideoConfig;
  111. import ch.threema.app.video.VideoTranscoder;
  112. import ch.threema.base.ThreemaException;
  113. import ch.threema.client.AbstractGroupMessage;
  114. import ch.threema.client.AbstractMessage;
  115. import ch.threema.client.BlobUploader;
  116. import ch.threema.client.BoxAudioMessage;
  117. import ch.threema.client.BoxImageMessage;
  118. import ch.threema.client.BoxLocationMessage;
  119. import ch.threema.client.BoxTextMessage;
  120. import ch.threema.client.BoxVideoMessage;
  121. import ch.threema.client.BoxedMessage;
  122. import ch.threema.client.ContactDeletePhotoMessage;
  123. import ch.threema.client.ContactRequestPhotoMessage;
  124. import ch.threema.client.ContactSetPhotoMessage;
  125. import ch.threema.client.DeliveryReceiptMessage;
  126. import ch.threema.client.GroupAudioMessage;
  127. import ch.threema.client.GroupImageMessage;
  128. import ch.threema.client.GroupLocationMessage;
  129. import ch.threema.client.GroupTextMessage;
  130. import ch.threema.client.GroupVideoMessage;
  131. import ch.threema.client.MessageId;
  132. import ch.threema.client.MessageQueue;
  133. import ch.threema.client.MessageTooLongException;
  134. import ch.threema.client.ProgressListener;
  135. import ch.threema.client.ProtocolDefines;
  136. import ch.threema.client.Utils;
  137. import ch.threema.client.ballot.BallotCreateInterface;
  138. import ch.threema.client.ballot.BallotCreateMessage;
  139. import ch.threema.client.ballot.GroupBallotCreateMessage;
  140. import ch.threema.client.file.FileData;
  141. import ch.threema.client.file.FileMessage;
  142. import ch.threema.client.file.FileMessageInterface;
  143. import ch.threema.client.file.GroupFileMessage;
  144. import ch.threema.localcrypto.MasterKey;
  145. import ch.threema.storage.DatabaseServiceNew;
  146. import ch.threema.storage.factories.GroupMessageModelFactory;
  147. import ch.threema.storage.factories.MessageModelFactory;
  148. import ch.threema.storage.models.AbstractMessageModel;
  149. import ch.threema.storage.models.ContactModel;
  150. import ch.threema.storage.models.DistributionListMessageModel;
  151. import ch.threema.storage.models.FirstUnreadMessageModel;
  152. import ch.threema.storage.models.GroupMessageModel;
  153. import ch.threema.storage.models.GroupMessagePendingMessageIdModel;
  154. import ch.threema.storage.models.GroupModel;
  155. import ch.threema.storage.models.MessageModel;
  156. import ch.threema.storage.models.MessageState;
  157. import ch.threema.storage.models.MessageType;
  158. import ch.threema.storage.models.ServerMessageModel;
  159. import ch.threema.storage.models.access.GroupAccessModel;
  160. import ch.threema.storage.models.ballot.BallotModel;
  161. import ch.threema.storage.models.data.LocationDataModel;
  162. import ch.threema.storage.models.data.MessageContentsType;
  163. import ch.threema.storage.models.data.media.AudioDataModel;
  164. import ch.threema.storage.models.data.media.BallotDataModel;
  165. import ch.threema.storage.models.data.media.FileDataModel;
  166. import ch.threema.storage.models.data.media.ImageDataModel;
  167. import ch.threema.storage.models.data.media.MediaMessageDataInterface;
  168. import ch.threema.storage.models.data.media.VideoDataModel;
  169. import ch.threema.storage.models.data.status.VoipStatusDataModel;
  170. import static ch.threema.app.ThreemaApplication.MAX_BLOB_SIZE;
  171. import static ch.threema.app.ThreemaApplication.getServiceManager;
  172. import static ch.threema.app.messagereceiver.MessageReceiver.Type_CONTACT;
  173. import static ch.threema.app.services.PreferenceService.ImageScale_DEFAULT;
  174. import static ch.threema.app.ui.MediaItem.TIME_UNDEFINED;
  175. import static ch.threema.app.ui.MediaItem.TYPE_FILE;
  176. import static ch.threema.app.ui.MediaItem.TYPE_GIF;
  177. import static ch.threema.app.ui.MediaItem.TYPE_IMAGE;
  178. import static ch.threema.app.ui.MediaItem.TYPE_IMAGE_CAM;
  179. import static ch.threema.app.ui.MediaItem.TYPE_TEXT;
  180. import static ch.threema.app.ui.MediaItem.TYPE_VIDEO;
  181. import static ch.threema.app.ui.MediaItem.TYPE_VIDEO_CAM;
  182. import static ch.threema.app.ui.MediaItem.TYPE_VOICEMESSAGE;
  183. import static ch.threema.client.file.FileData.RENDERING_STICKER;
  184. public class MessageServiceImpl implements MessageService {
  185. private static final Logger logger = LoggerFactory.getLogger(MessageServiceImpl.class);
  186. private final MessageQueue messageQueue;
  187. public static final String MESSAGE_QUEUE_SAVE_FILE = "msgqueue.ser";
  188. public static final long FILE_AUTO_DOWNLOAD_MAX_SIZE_M = 5; // MB
  189. public static final long FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO = FILE_AUTO_DOWNLOAD_MAX_SIZE_M * 1024 * 1024; // used for calculations
  190. public static final long FILE_AUTO_DOWNLOAD_MAX_SIZE_SI = FILE_AUTO_DOWNLOAD_MAX_SIZE_M * 1000 * 1000; // used for presentation only
  191. public static final int THUMBNAIL_SIZE_PX = 512;
  192. private final MessageSendingService messageSendingService;
  193. private final DatabaseServiceNew databaseServiceNew;
  194. private final ContactService contactService;
  195. private final FileService fileService;
  196. private final IdentityStore identityStore;
  197. private final Context context;
  198. private final MessageAckProcessor messageAckProcessor;
  199. private final BallotService ballotService;
  200. private final PreferenceService preferenceService;
  201. private final Collection<MessageModel> contactMessageCache;
  202. private final Collection<GroupMessageModel> groupMessageCache;
  203. private final Collection<DistributionListMessageModel> distributionListMessageCache;
  204. private final SparseIntArray loadingProgress = new SparseIntArray();
  205. private final LockAppService appLockService;
  206. private final GroupService groupService;
  207. private final ApiService apiService;
  208. private final DownloadService downloadService;
  209. private final DeadlineListService hiddenChatsListService;
  210. private final IdListService profilePicRecipientsService;
  211. public MessageServiceImpl(Context context,
  212. CacheService cacheService,
  213. MessageQueue messageQueue,
  214. DatabaseServiceNew databaseServiceNew,
  215. ContactService contactService,
  216. FileService fileService,
  217. IdentityStore identityStore,
  218. PreferenceService preferenceService,
  219. MessageAckProcessor messageAckProcessor,
  220. LockAppService appLockService,
  221. BallotService ballotService,
  222. GroupService groupService,
  223. ApiService apiService,
  224. DownloadService downloadService,
  225. DeadlineListService hiddenChatsListService,
  226. IdListService profilePicRecipientsService) {
  227. this.context = context;
  228. this.messageQueue = messageQueue;
  229. this.databaseServiceNew = databaseServiceNew;
  230. this.contactService = contactService;
  231. this.fileService = fileService;
  232. this.identityStore = identityStore;
  233. this.preferenceService = preferenceService;
  234. this.messageAckProcessor = messageAckProcessor;
  235. this.appLockService = appLockService;
  236. this.ballotService = ballotService;
  237. this.groupService = groupService;
  238. this.apiService = apiService;
  239. this.downloadService = downloadService;
  240. this.hiddenChatsListService = hiddenChatsListService;
  241. this.profilePicRecipientsService = profilePicRecipientsService;
  242. this.contactMessageCache = cacheService.getMessageModelCache();
  243. this.groupMessageCache = cacheService.getGroupMessageModelCache();
  244. this.distributionListMessageCache = cacheService.getDistributionListMessageCache();
  245. //init queue
  246. this.messageSendingService = new MessageSendingServiceExponentialBackOff(new MessageSendingService.MessageSendingServiceState() {
  247. @Override
  248. public void processingFinished(AbstractMessageModel messageModel, MessageReceiver receiver) {
  249. boolean setSent = false;
  250. messageModel.setSaved(true);
  251. receiver.saveLocalModel(messageModel);
  252. if (messageModel.getApiMessageId() != null && messageModel.getApiMessageId().length() > 0) {
  253. try {
  254. /* at this point, it is possible that the ACK from the server has already arrived before we had
  255. a chance to update the message ID in the model. Therefore we ask the message ACK processor
  256. whether the ACK has already been received before we update the state.
  257. */
  258. MessageId messageId = new MessageId(Utils.hexStringToByteArray(messageModel.getApiMessageId()));
  259. setSent = MessageServiceImpl.this.messageAckProcessor.isMessageIdAcked(messageId);
  260. } catch (ThreemaException e) {
  261. //do nothing an dont set as sent
  262. logger.error("Exception", e);
  263. }
  264. } else {
  265. setSent = true;
  266. }
  267. if (setSent) {
  268. updateMessageState(messageModel, MessageState.SENT, null);
  269. } else {
  270. updateMessageState(messageModel, MessageState.SENDING, null);
  271. }
  272. }
  273. @Override
  274. public void processingFailed(AbstractMessageModel messageModel, MessageReceiver receiver) {
  275. //remove send machine
  276. removeSendMachine(messageModel);
  277. updateMessageState(messageModel, MessageState.SENDFAILED, null);
  278. }
  279. @Override
  280. public void exception(Exception x, int tries) {
  281. if (tries >= 5) {
  282. logger.error("Exception", x);
  283. }
  284. }
  285. });
  286. /* read message queue from save file, if it exists */
  287. readMessageQueue();
  288. }
  289. private void cache(AbstractMessageModel m) {
  290. if (m instanceof GroupMessageModel) {
  291. synchronized (this.groupMessageCache) {
  292. this.groupMessageCache.add((GroupMessageModel) m);
  293. }
  294. } else if (m instanceof MessageModel) {
  295. synchronized (this.contactMessageCache) {
  296. this.contactMessageCache.add((MessageModel) m);
  297. }
  298. }
  299. }
  300. @Override
  301. public AbstractMessageModel createStatusMessage(String statusMessage, MessageReceiver receiver) {
  302. AbstractMessageModel model = receiver.createAndSaveStatusModel(statusMessage, new Date());
  303. this.fireOnCreatedMessage(model);
  304. return model;
  305. }
  306. @Override
  307. public AbstractMessageModel createVoipStatus(
  308. VoipStatusDataModel data,
  309. MessageReceiver receiver,
  310. boolean isOutbox,
  311. boolean isRead) {
  312. logger.info("Storing voip status message (outbox={}, status={}, reason={})",
  313. isOutbox, data.getStatus(), data.getReason());
  314. final AbstractMessageModel model = receiver.createLocalModel(
  315. MessageType.VOIP_STATUS,
  316. MessageContentsType.VOIP_STATUS,
  317. new Date()
  318. );
  319. model.setOutbox(isOutbox);
  320. model.setVoipStatusData(data);
  321. model.setSaved(true);
  322. model.setRead(isRead);
  323. receiver.saveLocalModel(model);
  324. this.fireOnCreatedMessage(model);
  325. return model;
  326. }
  327. public AbstractMessageModel createNewBallotMessage(
  328. MessageId messageId,
  329. BallotModel ballotModel,
  330. BallotDataModel.Type type,
  331. MessageReceiver receiver) {
  332. AbstractMessageModel model = receiver.createLocalModel(MessageType.BALLOT, MessageContentsType.BALLOT, new Date());
  333. if (model != null) {
  334. //hack: save ballot id into body string
  335. model.setIdentity(ballotModel.getCreatorIdentity());
  336. model.setSaved(true);
  337. model.setBallotData(new BallotDataModel(type, ballotModel.getId()));
  338. model.setOutbox(ballotModel.getCreatorIdentity().equals(this.identityStore.getIdentity()));
  339. model.setApiMessageId(messageId.toString());
  340. receiver.saveLocalModel(model);
  341. this.cache(model);
  342. this.fireOnCreatedMessage(model);
  343. }
  344. return model;
  345. }
  346. @Override
  347. public AbstractMessageModel sendText(String message, MessageReceiver messageReceiver) throws Exception {
  348. final String tag = "sendTextMessage";
  349. logger.info(tag + ": start");
  350. /* strip leading/trailing whitespace and ignore if nothing is left */
  351. String trimmedMessage = message.trim();
  352. if (trimmedMessage.length() == 0)
  353. return null;
  354. /* check maximum length in bytes (can be reached quickly with Unicode emojis etc.) */
  355. if (message.getBytes(StandardCharsets.UTF_8).length > ProtocolDefines.MAX_TEXT_MESSAGE_LEN)
  356. throw new MessageTooLongException();
  357. logger.debug(tag + ": create model instance");
  358. AbstractMessageModel messageModel = messageReceiver.createLocalModel(MessageType.TEXT, MessageContentsType.TEXT, new Date());
  359. logger.debug(tag + ": cache");
  360. this.cache(messageModel);
  361. messageModel.setOutbox(true);
  362. messageModel.setBodyAndQuotedMessageId(trimmedMessage);
  363. messageModel.setState(messageReceiver.sendMediaData() ? MessageState.SENDING : MessageState.SENT);
  364. messageModel.setSaved(true);
  365. logger.debug(tag + ": save db");
  366. messageReceiver.saveLocalModel(messageModel);
  367. logger.debug(tag + ": fire create message");
  368. this.fireOnCreatedMessage(messageModel);
  369. try {
  370. if (messageReceiver.createBoxedTextMessage(trimmedMessage, messageModel)) {
  371. String messageId = messageModel.getApiMessageId();
  372. logger.info(tag + ": message " + (messageId != null ? messageId : messageModel.getId()) + " successfully queued");
  373. } else {
  374. messageModel.setState(MessageState.SENDFAILED);
  375. }
  376. messageReceiver.saveLocalModel(messageModel);
  377. } catch (ThreemaException e) {
  378. messageModel.setState(MessageState.SENDFAILED);
  379. messageReceiver.saveLocalModel(messageModel);
  380. throw e;
  381. }
  382. this.fireOnModifiedMessage(messageModel);
  383. return messageModel;
  384. }
  385. @Override
  386. public AbstractMessageModel sendLocation(Location location, String poiName, MessageReceiver receiver, final CompletionHandler completionHandler) throws ThreemaException, IOException {
  387. final String tag = "sendLocationMessage";
  388. logger.info(tag + ": start");
  389. AbstractMessageModel messageModel = receiver.createLocalModel(MessageType.LOCATION, MessageContentsType.LOCATION, new Date());
  390. this.cache(messageModel);
  391. String address = null;
  392. try {
  393. address = GeoLocationUtil.getAddressFromLocation(context, location.getLatitude(), location.getLongitude());
  394. } catch (IOException e) {
  395. logger.error("Exception", e);
  396. //do not show this error!
  397. }
  398. messageModel.setLocationData(new LocationDataModel(
  399. location.getLatitude(),
  400. location.getLongitude(),
  401. (long) location.getAccuracy(),
  402. address,
  403. poiName
  404. ));
  405. messageModel.setOutbox(true);
  406. messageModel.setState(MessageState.PENDING);
  407. messageModel.setSaved(true);
  408. receiver.saveLocalModel(messageModel);
  409. this.fireOnCreatedMessage(messageModel);
  410. receiver.createBoxedLocationMessage(
  411. location.getLatitude(),
  412. location.getLongitude(),
  413. location.getAccuracy(),
  414. poiName,
  415. messageModel);
  416. messageModel.setState(receiver.sendMediaData() ? MessageState.SENDING : MessageState.SENT);
  417. receiver.saveLocalModel(messageModel);
  418. this.fireOnModifiedMessage(messageModel);
  419. if (completionHandler != null)
  420. completionHandler.sendQueued(messageModel);
  421. return messageModel;
  422. }
  423. private Set<ContactModel> addProfilePicRecipient(Set<ContactModel> contacts, ContactModel contact, UserService userService, Date lastUpdated) {
  424. if (contact != null) {
  425. String identity = contact.getIdentity();
  426. if (!userService.getIdentity().equals(identity)) {
  427. if (preferenceService.getProfilePicRelease() == PreferenceService.PROFILEPIC_RELEASE_EVERYONE ||
  428. (preferenceService.getProfilePicRelease() == PreferenceService.PROFILEPIC_RELEASE_SOME &&
  429. profilePicRecipientsService.has(identity))) {
  430. Date date = contact.getProfilePicSentDate();
  431. if (date == null || lastUpdated.after(date)) {
  432. contacts.add(contact);
  433. }
  434. }
  435. }
  436. }
  437. return contacts;
  438. }
  439. @Override
  440. public boolean sendProfilePicture(MessageReceiver[] messageReceivers) {
  441. if (messageReceivers.length > 0) {
  442. Date lastUpdated = preferenceService.getProfilePicLastUpdate();
  443. if (lastUpdated == null) {
  444. return false;
  445. }
  446. UserService userService;
  447. try {
  448. userService = ThreemaApplication.getServiceManager().getUserService();
  449. if (userService == null) {
  450. return false;
  451. }
  452. } catch (Exception e) {
  453. return false;
  454. }
  455. // create array of receivers that need an update
  456. Set<ContactModel> outdatedContacts = new HashSet<>();
  457. Set<ContactModel> restoredContacts = new HashSet<>();
  458. for (MessageReceiver messageReceiver : messageReceivers) {
  459. if (messageReceiver instanceof ContactMessageReceiver) {
  460. ContactModel contactModel = ((ContactMessageReceiver) messageReceiver).getContact();
  461. if (contactModel.isRestored()) {
  462. restoredContacts.add(contactModel);
  463. }
  464. if (!ContactUtil.canReceiveProfilePics(contactModel)) {
  465. continue;
  466. }
  467. outdatedContacts = addProfilePicRecipient(outdatedContacts, contactModel, userService, lastUpdated);
  468. } else if (messageReceiver instanceof GroupMessageReceiver) {
  469. GroupModel groupModel = ((GroupMessageReceiver) messageReceiver).getGroup();
  470. if (groupModel != null) {
  471. for (ContactModel contactModel : groupService.getMembers(groupModel)) {
  472. if (contactModel.isRestored()) {
  473. restoredContacts.add(contactModel);
  474. }
  475. outdatedContacts = addProfilePicRecipient(outdatedContacts, contactModel, userService, lastUpdated);
  476. }
  477. }
  478. }
  479. }
  480. if (restoredContacts.size() > 0) {
  481. /* as the other party doesn't know that we restored his contact from a backup we send him a request profile photo
  482. * message causing him to re-send the profile pic at his earliest convenience, i.e. accompanying a regular message
  483. * */
  484. for (ContactModel contactModel : restoredContacts) {
  485. ContactRequestPhotoMessage msg = new ContactRequestPhotoMessage();
  486. msg.setToIdentity(contactModel.getIdentity());
  487. logger.info("Enqueue request profile picture message ID {} to {}", msg.getMessageId(), msg.getToIdentity());
  488. BoxedMessage boxedMessage = null;
  489. try {
  490. boxedMessage = this.messageQueue.enqueue(msg);
  491. } catch (ThreemaException e) {
  492. logger.error("Exception", e);
  493. }
  494. if (boxedMessage != null) {
  495. contactModel.setIsRestored(false);
  496. contactService.save(contactModel);
  497. }
  498. }
  499. }
  500. if (preferenceService.getProfilePicRelease() != PreferenceService.PROFILEPIC_RELEASE_NOBODY) {
  501. if (outdatedContacts.size() > 0) {
  502. String tag = "sendProfileImageMessage";
  503. logger.info(tag + ": start");
  504. ContactModel myContactModel = contactService.getByIdentity(userService.getIdentity());
  505. Bitmap image = contactService.getAvatar(myContactModel, true, false);
  506. if (image != null) {
  507. try {
  508. ContactServiceImpl.ContactPhotoUploadResult result = contactService.uploadContactPhoto(image);
  509. for (ContactModel contactModel : outdatedContacts) {
  510. ContactSetPhotoMessage msg = new ContactSetPhotoMessage();
  511. msg.setBlobId(result.blobId);
  512. msg.setEncryptionKey(result.encryptionKey);
  513. msg.setSize(result.size);
  514. msg.setToIdentity(contactModel.getIdentity());
  515. logger.info("Enqueue profile picture message ID {} to {}", msg.getMessageId(), msg.getToIdentity());
  516. BoxedMessage boxedMessage = this.messageQueue.enqueue(msg);
  517. if (boxedMessage != null) {
  518. contactModel.setProfilePicSentDate(new Date());
  519. contactService.save(contactModel);
  520. }
  521. }
  522. } catch (Exception e) {
  523. logger.error("Exception", e);
  524. }
  525. } else {
  526. // local avatar has been removed - send a Delete Photo message
  527. for (ContactModel contactModel : outdatedContacts) {
  528. ContactDeletePhotoMessage msg = new ContactDeletePhotoMessage();
  529. msg.setToIdentity(contactModel.getIdentity());
  530. logger.info("Enqueue remove profile picture message ID {} to {}", msg.getMessageId(), msg.getToIdentity());
  531. try {
  532. BoxedMessage boxedMessage = this.messageQueue.enqueue(msg);
  533. if (boxedMessage != null) {
  534. contactModel.setProfilePicSentDate(new Date());
  535. contactService.save(contactModel);
  536. }
  537. } catch (ThreemaException e) {
  538. logger.error("Exception", e);
  539. }
  540. }
  541. }
  542. }
  543. }
  544. }
  545. //invalid image
  546. return false;
  547. }
  548. @Override
  549. @WorkerThread
  550. public void resendMessage(AbstractMessageModel messageModel, MessageReceiver receiver, CompletionHandler completionHandler) throws Exception {
  551. NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
  552. notificationManager.cancel(ThreemaApplication.UNSENT_MESSAGE_NOTIFICATION_ID);
  553. if (messageModel.getState() == MessageState.SENDFAILED) {
  554. resendFileMessage(messageModel, receiver, completionHandler);
  555. }
  556. }
  557. @WorkerThread
  558. private void resendFileMessage(final AbstractMessageModel messageModel,
  559. final MessageReceiver receiver,
  560. final CompletionHandler completionHandler) throws Exception {
  561. // check if a message file exists that could be resent or abort immediately
  562. File file = fileService.getMessageFile(messageModel);
  563. if (file == null || !file.exists()) {
  564. throw new Exception("Message file not present");
  565. }
  566. updateMessageState(messageModel, MessageState.PENDING, new Date());
  567. //enqueue processing and uploading stuff...
  568. this.messageSendingService.addToQueue(new MessageSendingService.MessageSendingProcess() {
  569. public byte[] blobIdThumbnail;
  570. public byte[] blobId;
  571. public byte[] thumbnailData;
  572. public byte[] fileData;
  573. public int fileDataBoxedLength;
  574. public MessageReceiver.EncryptResult thumbnailEncryptResult;
  575. public MessageReceiver.EncryptResult encryptResult;
  576. public boolean success = false;
  577. @Override
  578. public MessageReceiver getReceiver() {
  579. return receiver;
  580. }
  581. @Override
  582. public AbstractMessageModel getMessageModel() {
  583. return messageModel;
  584. }
  585. @Override
  586. public boolean send() throws Exception {
  587. SendMachine sendMachine = getSendMachine(messageModel);
  588. sendMachine.reset()
  589. .next(new SendMachineProcess() {
  590. @Override
  591. public void run() throws Exception {
  592. File decryptedMessageFile = fileService.getDecryptedMessageFile(messageModel);
  593. if (decryptedMessageFile != null) {
  594. try (FileInputStream inputStream = new FileInputStream(decryptedMessageFile)) {
  595. fileDataBoxedLength = inputStream.available();
  596. fileData = new byte[fileDataBoxedLength + NaCl.BOXOVERHEAD];
  597. IOUtils.readFully(inputStream,
  598. fileData,
  599. NaCl.BOXOVERHEAD,
  600. fileDataBoxedLength);
  601. }
  602. } else {
  603. throw new Exception("Message file not present");
  604. }
  605. }
  606. })
  607. .next(new SendMachineProcess() {
  608. @Override
  609. public void run() throws Exception {
  610. encryptResult = getReceiver().encryptFileData(fileData);
  611. if (encryptResult.getData() == null || encryptResult.getSize() == 0) {
  612. throw new Exception("File data encrypt failed");
  613. }
  614. }
  615. })
  616. .next(new SendMachineProcess() {
  617. @Override
  618. public void run() throws Exception {
  619. try (InputStream is = fileService.getDecryptedMessageThumbnailStream(messageModel)) {
  620. if (is != null) {
  621. thumbnailData = IOUtils.toByteArray(is);
  622. } else {
  623. thumbnailData = null;
  624. }
  625. } catch (Exception e) {
  626. logger.debug("No thumbnail for file message");
  627. }
  628. }
  629. })
  630. .next(new SendMachineProcess() {
  631. @Override
  632. public void run() throws Exception {
  633. BlobUploader blobUploader = initUploader(getMessageModel(), encryptResult.getData());
  634. blobUploader.setProgressListener(new ProgressListener() {
  635. @Override
  636. public void updateProgress(int progress) {
  637. updateMessageLoadingProgress(messageModel, progress);
  638. }
  639. @Override
  640. public void onFinished(boolean success) {
  641. setMessageLoadingFinished(messageModel, success);
  642. }
  643. });
  644. blobId = blobUploader.upload();
  645. }
  646. })
  647. .next(new SendMachineProcess() {
  648. @Override
  649. public void run() throws Exception {
  650. if (thumbnailData != null) {
  651. thumbnailEncryptResult = getReceiver().encryptFileThumbnailData(thumbnailData, encryptResult.getKey());
  652. if (thumbnailEncryptResult.getData() != null) {
  653. BlobUploader blobUploader = initUploader(getMessageModel(), thumbnailEncryptResult.getData());
  654. blobUploader.setProgressListener(new ProgressListener() {
  655. @Override
  656. public void updateProgress(int progress) {
  657. updateMessageLoadingProgress(messageModel, progress);
  658. }
  659. @Override
  660. public void onFinished(boolean success) {
  661. setMessageLoadingFinished(messageModel, success);
  662. }
  663. });
  664. blobIdThumbnail = blobUploader.upload();
  665. } else {
  666. throw new Exception("Thumbnail encrypt failed");
  667. }
  668. }
  669. }
  670. })
  671. .next(new SendMachineProcess() {
  672. @Override
  673. public void run() throws Exception {
  674. getReceiver().createBoxedFileMessage(
  675. blobIdThumbnail,
  676. blobId,
  677. encryptResult,
  678. messageModel
  679. );
  680. save(messageModel);
  681. }
  682. })
  683. .next(new SendMachineProcess() {
  684. @Override
  685. public void run() throws Exception {
  686. updateMessageState(messageModel, MessageState.SENDING, null);
  687. if (completionHandler != null)
  688. completionHandler.sendComplete(messageModel);
  689. success = true;
  690. }
  691. });
  692. if (this.success) {
  693. removeSendMachine(sendMachine);
  694. }
  695. return this.success;
  696. }
  697. });
  698. }
  699. @Override
  700. public AbstractMessageModel sendBallotMessage(BallotModel ballotModel) throws MessageTooLongException {
  701. //create a new ballot model
  702. if(ballotModel != null) {
  703. MessageReceiver receiver = this.ballotService.getReceiver(ballotModel);
  704. if(receiver != null) {
  705. //ok...
  706. logger.debug("sendBallotMessage ", receiver.toString());
  707. final AbstractMessageModel messageModel = receiver.createLocalModel(MessageType.BALLOT, MessageContentsType.BALLOT, new Date());
  708. this.cache(messageModel);
  709. messageModel.setOutbox(true);
  710. messageModel.setState(MessageState.PENDING);
  711. messageModel.setBallotData(new BallotDataModel(
  712. ballotModel.getState() == BallotModel.State.OPEN ?
  713. BallotDataModel.Type.BALLOT_CREATED :
  714. BallotDataModel.Type.BALLOT_CLOSED,
  715. ballotModel.getId()));
  716. messageModel.setSaved(true);
  717. receiver.saveLocalModel(messageModel);
  718. this.fireOnCreatedMessage(messageModel);
  719. this.resendBallotMessage(messageModel, ballotModel, receiver);
  720. return messageModel;
  721. }
  722. }
  723. return null;
  724. }
  725. private void resendBallotMessage(AbstractMessageModel messageModel, BallotModel ballotModel, MessageReceiver receiver) throws MessageTooLongException {
  726. //get ballot data
  727. if(!TestUtil.required(messageModel, ballotModel, receiver)) {
  728. return;
  729. }
  730. this.updateMessageState(messageModel, MessageState.PENDING, new Date());
  731. try {
  732. this.ballotService.publish(receiver, ballotModel, messageModel);
  733. }
  734. catch (NotAllowedException | MessageTooLongException x) {
  735. logger.error("Exception", x);
  736. if (x instanceof MessageTooLongException) {
  737. this.remove(messageModel);
  738. this.fireOnRemovedMessage(messageModel);
  739. throw new MessageTooLongException();
  740. } else {
  741. this.updateMessageState(messageModel, MessageState.SENDFAILED, new Date());
  742. }
  743. }
  744. }
  745. @Override
  746. public boolean sendUserAcknowledgement(AbstractMessageModel messageModel) {
  747. if (MessageUtil.canSendUserAcknowledge(messageModel)) {
  748. DeliveryReceiptMessage receipt = new DeliveryReceiptMessage();
  749. receipt.setReceiptType(ProtocolDefines.DELIVERYRECEIPT_MSGUSERACK);
  750. try {
  751. receipt.setReceiptMessageIds(new MessageId[]{new MessageId(Utils.hexStringToByteArray(messageModel.getApiMessageId()))});
  752. receipt.setFromIdentity(this.identityStore.getIdentity());
  753. receipt.setToIdentity(messageModel.getIdentity());
  754. logger.info("Enqueue delivery receipt (user ack) message ID {} for message ID {} from {}",
  755. receipt.getMessageId(), receipt.getReceiptMessageIds()[0], receipt.getToIdentity());
  756. this.messageQueue.enqueue(receipt);
  757. messageModel.setState(MessageState.USERACK);
  758. this.save(messageModel);
  759. this.fireOnModifiedMessage(messageModel);
  760. return true;
  761. } catch (ThreemaException e) {
  762. logger.error("Exception", e);
  763. }
  764. }
  765. return false;
  766. }
  767. @Override
  768. public boolean sendUserDecline(AbstractMessageModel messageModel) {
  769. if (MessageUtil.canSendUserDecline(messageModel)) {
  770. DeliveryReceiptMessage receipt = new DeliveryReceiptMessage();
  771. receipt.setReceiptType(ProtocolDefines.DELIVERYRECEIPT_MSGUSERDEC);
  772. try {
  773. receipt.setReceiptMessageIds(new MessageId[]{new MessageId(Utils.hexStringToByteArray(messageModel.getApiMessageId()))});
  774. receipt.setFromIdentity(this.identityStore.getIdentity());
  775. receipt.setToIdentity(messageModel.getIdentity());
  776. logger.info("Enqueue delivery receipt (user dec) message ID {} for message ID {} from {}",
  777. receipt.getMessageId(), receipt.getReceiptMessageIds()[0], receipt.getToIdentity());
  778. this.messageQueue.enqueue(receipt);
  779. messageModel.setState(MessageState.USERDEC);
  780. this.save(messageModel);
  781. this.fireOnModifiedMessage(messageModel);
  782. return true;
  783. } catch (ThreemaException e) {
  784. logger.error("Exception", e);
  785. }
  786. }
  787. return false;
  788. }
  789. private AbstractMessageModel getAbstractMessageModelByApiIdAndIdentity(final MessageId apiMessageId, final String identity) {
  790. //contact message cache
  791. synchronized (this.contactMessageCache) {
  792. AbstractMessageModel messageModel = Functional.select(this.contactMessageCache, new IPredicateNonNull<MessageModel>() {
  793. @Override
  794. public boolean apply(@NonNull MessageModel m) {
  795. return m.getApiMessageId() != null
  796. && m.getApiMessageId().equals(apiMessageId.toString())
  797. && TestUtil.compare(m.getIdentity(), identity);
  798. }
  799. });
  800. if(messageModel != null) {
  801. return messageModel;
  802. }
  803. }
  804. //group message cache
  805. synchronized (this.groupMessageCache) {
  806. AbstractMessageModel messageModel = Functional.select(this.groupMessageCache, new IPredicateNonNull<GroupMessageModel>() {
  807. @Override
  808. public boolean apply(@NonNull GroupMessageModel m) {
  809. return m.getApiMessageId() != null
  810. && m.getApiMessageId().equals(apiMessageId.toString())
  811. && TestUtil.compare(m.getIdentity(), identity);
  812. }
  813. });
  814. if(messageModel != null) {
  815. return messageModel;
  816. }
  817. }
  818. MessageModel contactMessageModel = this.databaseServiceNew.getMessageModelFactory().getByApiMessageIdAndIdentity(
  819. apiMessageId,
  820. identity);
  821. if(contactMessageModel != null) {
  822. this.cache(contactMessageModel);
  823. return contactMessageModel;
  824. }
  825. GroupMessageModel groupMessageModel = this.databaseServiceNew.getGroupMessageModelFactory().getByApiMessageIdAndIdentity(apiMessageId, identity);
  826. if(groupMessageModel != null) {
  827. this.cache(groupMessageModel);
  828. return groupMessageModel;
  829. }
  830. return null;
  831. }
  832. private AbstractMessageModel getAbstractMessageModelByApiIdAndOutbox(final MessageId apiMessageId)
  833. {
  834. //contact message cache
  835. synchronized (this.contactMessageCache) {
  836. AbstractMessageModel messageModel = Functional.select(this.contactMessageCache, new IPredicateNonNull<MessageModel>() {
  837. @Override
  838. public boolean apply(@NonNull MessageModel m) {
  839. return m.getApiMessageId() != null
  840. && m.getApiMessageId().equals(apiMessageId.toString())
  841. && m.isOutbox();
  842. }
  843. });
  844. if(messageModel != null) {
  845. return messageModel;
  846. }
  847. }
  848. //group message cache
  849. synchronized (this.groupMessageCache) {
  850. AbstractMessageModel messageModel = Functional.select(this.groupMessageCache, new IPredicateNonNull<GroupMessageModel>() {
  851. @Override
  852. public boolean apply(@NonNull GroupMessageModel m) {
  853. return m.getApiMessageId() != null
  854. && m.getApiMessageId().equals(apiMessageId.toString())
  855. && m.isOutbox();
  856. }
  857. });
  858. if(messageModel != null) {
  859. return messageModel;
  860. }
  861. }
  862. MessageModel contactMessageModel = this.databaseServiceNew.getMessageModelFactory().getByApiMessageIdAndIsOutbox(
  863. apiMessageId,
  864. true);
  865. if(contactMessageModel != null) {
  866. this.cache(contactMessageModel);
  867. return contactMessageModel;
  868. }
  869. GroupMessageModel groupMessageModel = this.databaseServiceNew.getGroupMessageModelFactory().getByApiMessageIdAndIsOutbox(
  870. apiMessageId,
  871. true);
  872. if(groupMessageModel != null) {
  873. this.cache(groupMessageModel);
  874. return groupMessageModel;
  875. }
  876. return null;
  877. }
  878. public void updateMessageState(final MessageId apiMessageId, String identity, MessageState state, Date stateDate) {
  879. AbstractMessageModel messageModel = this.getAbstractMessageModelByApiIdAndIdentity(apiMessageId, identity);
  880. if(messageModel == null) {
  881. //try to select a group message
  882. GroupMessagePendingMessageIdModel groupMessagePendingMessageIdModel = this.databaseServiceNew
  883. .getGroupMessagePendingMessageIdModelFactory().get(apiMessageId.toString());
  884. if(groupMessagePendingMessageIdModel != null) {
  885. this.updateMessageState(groupMessagePendingMessageIdModel, state, stateDate);
  886. }
  887. }
  888. else {
  889. this.updateMessageState(messageModel, state, stateDate);
  890. }
  891. }
  892. @Override
  893. public void updateMessageStateAtOutboxed(MessageId apiMessageId, MessageState state, Date stateDate) {
  894. AbstractMessageModel messageModel = this.getAbstractMessageModelByApiIdAndOutbox(apiMessageId);
  895. if(messageModel == null) {
  896. //try to select a group message
  897. GroupMessagePendingMessageIdModel groupMessagePendingMessageIdModel = this.databaseServiceNew
  898. .getGroupMessagePendingMessageIdModelFactory().get(apiMessageId.toString());
  899. if(groupMessagePendingMessageIdModel != null) {
  900. this.updateMessageState(groupMessagePendingMessageIdModel, state, stateDate);
  901. }
  902. }
  903. else {
  904. this.updateMessageState(messageModel, state, stateDate);
  905. }
  906. }
  907. private void updateMessageState(AbstractMessageModel messageModel, MessageState state, Date stateDate) {
  908. synchronized (this) {
  909. if(MessageUtil.canChangeToState(messageModel.getState(), state, messageModel.isOutbox())) {
  910. messageModel.setState(state);
  911. if (stateDate != null) {
  912. messageModel.setModifiedAt(stateDate);
  913. }
  914. this.save(messageModel);
  915. this.fireOnModifiedMessage(messageModel);
  916. }
  917. }
  918. }
  919. private void updateMessageState(GroupMessagePendingMessageIdModel groupMessagePendingMessageIdModel, MessageState state, Date stateDate) {
  920. logger.debug("update pending group message id to " + state);
  921. GroupMessageModel groupMessageModel = this.getGroupMessageModel(groupMessagePendingMessageIdModel.getGroupMessageId(), true);
  922. if(groupMessageModel != null) {
  923. if(state == MessageState.SENT) {
  924. //remove from pending group
  925. this.databaseServiceNew.getGroupMessagePendingMessageIdModelFactory()
  926. .delete(groupMessagePendingMessageIdModel);
  927. logger.debug("removed...");
  928. //check if the pending list is empty
  929. long pendingCount = this.databaseServiceNew.getGroupMessagePendingMessageIdModelFactory()
  930. .countByGroupMessage(groupMessageModel.getId());
  931. logger.debug("new count = " + pendingCount);
  932. if(pendingCount == 0) {
  933. //set the group message as sent
  934. this.updateMessageState(groupMessageModel, MessageState.SENT, stateDate);
  935. }
  936. }
  937. }
  938. else {
  939. logger.debug("no group message found! groupMessagePendingMessageIdModel.id = " + groupMessagePendingMessageIdModel.getGroupMessageId());
  940. }
  941. }
  942. @Override
  943. public boolean markAsRead(AbstractMessageModel message, boolean silent) throws ThreemaException {
  944. logger.debug("markAsRead message = " + message.getApiMessageId() + " silent = " + silent);
  945. boolean saved = false;
  946. if (MessageUtil.canMarkAsRead(message)) {
  947. boolean sendDeliveryReceipt = MessageUtil.canSendDeliveryReceipt(message)
  948. && this.preferenceService.isReadReceipts();
  949. //save is read
  950. message.setRead(true);
  951. message.setModifiedAt(new Date());
  952. this.save(message);
  953. if(!silent) {
  954. //fire on modified if not silent
  955. this.fireOnModifiedMessage(message);
  956. }
  957. saved = true;
  958. if (sendDeliveryReceipt) {
  959. DeliveryReceiptMessage receipt = new DeliveryReceiptMessage();
  960. receipt.setReceiptType(ProtocolDefines.DELIVERYRECEIPT_MSGREAD);
  961. receipt.setReceiptMessageIds(new MessageId[]{new MessageId(Utils.hexStringToByteArray(message.getApiMessageId()))});
  962. receipt.setFromIdentity(this.identityStore.getIdentity());
  963. receipt.setToIdentity(message.getIdentity());
  964. logger.info("Enqueue delivery receipt (read) message ID {} for message ID {} from {}",
  965. receipt.getMessageId(), receipt.getReceiptMessageIds()[0], receipt.getToIdentity());
  966. this.messageQueue.enqueue(receipt);
  967. }
  968. }
  969. return saved;
  970. }
  971. @Override
  972. public void remove(AbstractMessageModel messageModel) {
  973. this.remove(messageModel, false);
  974. }
  975. @Override
  976. public void remove(final AbstractMessageModel messageModel, boolean silent) {
  977. SendMachine machine = this.getSendMachine(messageModel);
  978. if(machine != null) {
  979. //abort pending send machine
  980. //do not remove SendMachine (fix ANDR-522)
  981. machine.abort();
  982. }
  983. //remove pending uploads
  984. this.cancelUploader(messageModel);
  985. //remove from sdcard
  986. this.fileService.removeMessageFiles(messageModel, true);
  987. // remove message from messageQueue
  988. if (messageModel.isOutbox() && messageModel.getApiMessageId() != null) {
  989. try {
  990. MessageId messageId = new MessageId(Utils.hexStringToByteArray(messageModel.getApiMessageId()));
  991. this.messageQueue.dequeue(messageId);
  992. } catch (ThreemaException e) {
  993. logger.error("Exception", e);
  994. }
  995. }
  996. //remove from dao
  997. if(messageModel instanceof GroupMessageModel) {
  998. this.databaseServiceNew.getGroupMessageModelFactory().delete(
  999. (GroupMessageModel) messageModel
  1000. );
  1001. //remove from cache
  1002. synchronized (this.groupMessageCache) {
  1003. Iterator<GroupMessageModel> i = this.groupMessageCache.iterator();
  1004. while(i.hasNext()) {
  1005. if(i.next().getId() == messageModel.getId()) {
  1006. i.remove();
  1007. }
  1008. }
  1009. }
  1010. this.databaseServiceNew.getGroupMessagePendingMessageIdModelFactory().delete(
  1011. messageModel.getId()
  1012. );
  1013. }
  1014. else if (messageModel instanceof DistributionListMessageModel) {
  1015. this.databaseServiceNew.getDistributionListMessageModelFactory().delete(
  1016. (DistributionListMessageModel) messageModel
  1017. );
  1018. //remove from cache
  1019. synchronized (this.distributionListMessageCache) {
  1020. Iterator<DistributionListMessageModel> i = this.distributionListMessageCache.iterator();
  1021. while(i.hasNext()) {
  1022. if(i.next().getId() == messageModel.getId()) {
  1023. i.remove();
  1024. }
  1025. }
  1026. }
  1027. }
  1028. else if (messageModel instanceof MessageModel) {
  1029. this.databaseServiceNew.getMessageModelFactory().delete((MessageModel) messageModel);
  1030. //remove from cache
  1031. synchronized (this.contactMessageCache) {
  1032. Iterator<MessageModel> i = this.contactMessageCache.iterator();
  1033. while(i.hasNext()) {
  1034. if(i.next().getId() == messageModel.getId()) {
  1035. i.remove();
  1036. }
  1037. }
  1038. }
  1039. }
  1040. if(!silent) {
  1041. this.fireOnRemovedMessage(messageModel);
  1042. }
  1043. }
  1044. @Override
  1045. public boolean processIncomingContactMessage(final AbstractMessage message) throws Exception {
  1046. logger.info("processIncomingContactMessage: {}", message.getMessageId());
  1047. MessageModel messageModel = null;
  1048. MessageModel existingModel = this.databaseServiceNew.getMessageModelFactory()
  1049. .getByApiMessageIdAndIdentity(message.getMessageId(), message.getFromIdentity());
  1050. logger.info("processIncomingContactMessage: {} - A", message.getMessageId());
  1051. if (existingModel != null) {
  1052. //first search in cache
  1053. MessageModel savedMessageModel;
  1054. logger.info("processIncomingContactMessage: {} check contact message cache", message.getMessageId());
  1055. synchronized (this.contactMessageCache) {
  1056. savedMessageModel = Functional.select(this.contactMessageCache, new IPredicateNonNull<MessageModel>() {
  1057. @Override
  1058. public boolean apply(@NonNull MessageModel messageModel1) {
  1059. return messageModel1.getApiMessageId() != null &&
  1060. messageModel1.getApiMessageId().equals(message.getMessageId().toString())
  1061. && message.getFromIdentity() != null
  1062. && message.getFromIdentity().equals(messageModel1.getIdentity());
  1063. }
  1064. });
  1065. }
  1066. logger.info("processIncomingContactMessage: {} check contact message cache end", message.getMessageId());
  1067. if(savedMessageModel == null) {
  1068. //get from sql result
  1069. savedMessageModel = existingModel;
  1070. }
  1071. if(savedMessageModel.isSaved()) {
  1072. //do nothing!
  1073. // TODO don't we need to send a delivery receipt here as well?
  1074. return true;
  1075. }
  1076. else {
  1077. messageModel = savedMessageModel;
  1078. }
  1079. }
  1080. if (message.getClass().equals(BoxTextMessage.class)) {
  1081. messageModel = this.saveBoxMessage((BoxTextMessage) message, messageModel);
  1082. }
  1083. else if (message.getClass().equals(BoxImageMessage.class)) {
  1084. messageModel = this.saveBoxMessage((BoxImageMessage) message, messageModel);
  1085. // silently save to gallery if enabled
  1086. if (messageModel != null
  1087. && preferenceService != null
  1088. && preferenceService.isSaveMedia()
  1089. && messageModel.getImageData().isDownloaded()
  1090. && !hiddenChatsListService.has(contactService.getUniqueIdString(messageModel.getIdentity()))) {
  1091. fileService.saveMedia(null, null, new CopyOnWriteArrayList<>(Collections.singletonList(messageModel)), true);
  1092. }
  1093. }
  1094. else if (message.getClass().equals(BoxVideoMessage.class)) {
  1095. messageModel = this.saveBoxMessage((BoxVideoMessage) message, messageModel);
  1096. }
  1097. else if (message.getClass().equals(BoxLocationMessage.class)) {
  1098. messageModel = this.saveBoxMessage((BoxLocationMessage) message, messageModel);
  1099. }
  1100. else if (message.getClass().equals(BoxAudioMessage.class)) {
  1101. messageModel = this.saveBoxMessage((BoxAudioMessage) message, messageModel);
  1102. }
  1103. else if (message.getClass().equals(BallotCreateMessage.class)) {
  1104. messageModel = this.saveBoxMessage((BallotCreateMessage) message, messageModel);
  1105. }
  1106. else if (message.getClass().equals(FileMessage.class)) {
  1107. messageModel = this.saveBoxMessage((FileMessage) message, messageModel);
  1108. }
  1109. if (null != messageModel) {
  1110. /* as soon as we get a direct message, unhide the contact */
  1111. this.contactService.setIsHidden(message.getFromIdentity(), false);
  1112. this.contactService.setIsArchived(message.getFromIdentity(), false);
  1113. //send msgreceived
  1114. if (!message.isNoDeliveryReceipts()) {
  1115. DeliveryReceiptMessage receipt = new DeliveryReceiptMessage();
  1116. receipt.setReceiptType(ProtocolDefines.DELIVERYRECEIPT_MSGRECEIVED);
  1117. receipt.setReceiptMessageIds(new MessageId[]{message.getMessageId()});
  1118. receipt.setFromIdentity(this.identityStore.getIdentity());
  1119. receipt.setToIdentity(message.getFromIdentity());
  1120. logger.info("Enqueue delivery receipt (delivered) message ID {} for message ID {} from {}",
  1121. receipt.getMessageId(), receipt.getReceiptMessageIds()[0], receipt.getToIdentity());
  1122. this.messageQueue.enqueue(receipt);
  1123. }
  1124. return true;
  1125. }
  1126. return false;
  1127. }
  1128. @Override
  1129. public boolean processIncomingGroupMessage(AbstractGroupMessage message) throws Exception {
  1130. logger.info("processIncomingGroupMessage: {}", message.getMessageId());
  1131. GroupMessageModel messageModel = null;
  1132. //first of all, check if i can receive messages
  1133. GroupModel groupModel = this.groupService.getGroup(message);
  1134. if(groupModel == null) {
  1135. logger.error("GroupMessage {}: error: no groupModel", message.getMessageId());
  1136. return false;
  1137. }
  1138. //is allowed?
  1139. GroupAccessModel access = this.groupService.getAccess(groupModel, false);
  1140. if(access == null ||
  1141. !access.getCanReceiveMessageAccess().isAllowed()) {
  1142. //not allowed to receive a message, ignore message but
  1143. //set success to true (remove from server)
  1144. logger.error("GroupMessage {}: error: not allowed", message.getMessageId());
  1145. return true;
  1146. }
  1147. if(this.groupService.getGroupMember(groupModel, message.getFromIdentity()) == null) {
  1148. // we received a group message from a user that is not or no longer part of the group
  1149. if(this.groupService.isGroupOwner(groupModel)) {
  1150. // send empty group create to the user if i am the group administrator
  1151. this.groupService.sendEmptySync(groupModel, message.getFromIdentity());
  1152. }
  1153. else {
  1154. // otherwise request a sync
  1155. this.groupService.requestSync(message, false);
  1156. }
  1157. logger.error("GroupMessage {}: error: contact is not in my group list", message.getMessageId());
  1158. return true;
  1159. }
  1160. // reset archived status
  1161. groupService.setIsArchived(groupModel, false);
  1162. GroupMessageModel existingModel = this.databaseServiceNew.getGroupMessageModelFactory().getByApiMessageIdAndIdentity(
  1163. message.getMessageId(),
  1164. message.getFromIdentity()
  1165. );
  1166. if (existingModel != null) {
  1167. if (existingModel.isSaved()) {
  1168. //do nothing!
  1169. logger.error("GroupMessage {}: error: message already exists", message.getMessageId());
  1170. return true;
  1171. } else {
  1172. //use the first non saved model to edit!
  1173. logger.error("GroupMessage {}: error: reusing unsaved model", message.getMessageId());
  1174. messageModel = existingModel;
  1175. }
  1176. }
  1177. if (message.getClass().equals(GroupTextMessage.class)) {
  1178. messageModel = this.saveGroupMessage((GroupTextMessage) message, messageModel);
  1179. }
  1180. else if (message.getClass().equals(GroupImageMessage.class)) {
  1181. messageModel = this.saveGroupMessage((GroupImageMessage) message, messageModel);
  1182. // silently save to gallery if enabled
  1183. if (messageModel != null
  1184. && preferenceService != null
  1185. && preferenceService.isSaveMedia()
  1186. && messageModel.getImageData().isDownloaded()
  1187. && !hiddenChatsListService.has(groupService.getUniqueIdString(groupModel))) {
  1188. fileService.saveMedia(null, null, new CopyOnWriteArrayList<>(Collections.singletonList(messageModel)), true);
  1189. }
  1190. }
  1191. else if (message.getClass().equals(GroupVideoMessage.class)) {
  1192. messageModel =this.saveGroupMessage((GroupVideoMessage) message, messageModel);
  1193. }
  1194. else if (message.getClass().equals(GroupLocationMessage.class)) {
  1195. messageModel =this.saveGroupMessage((GroupLocationMessage) message, messageModel);
  1196. }
  1197. else if (message.getClass().equals(GroupAudioMessage.class)) {
  1198. messageModel = this.saveGroupMessage((GroupAudioMessage) message, messageModel);
  1199. }
  1200. else if (message.getClass().equals(GroupBallotCreateMessage.class)) {
  1201. messageModel = this.saveGroupMessage((GroupBallotCreateMessage) message, messageModel);
  1202. }
  1203. else if(message.getClass().equals(GroupFileMessage.class)) {
  1204. messageModel = this.saveGroupMessage((GroupFileMessage) message, messageModel);
  1205. }
  1206. logger.info("processIncomingGroupMessage: {} success = {}", message.getMessageId(), messageModel != null);
  1207. return messageModel != null;
  1208. }
  1209. private MessageModel saveBoxMessage(BoxTextMessage message, MessageModel messageModel) throws Exception {
  1210. ContactModel contactModel = this.contactService.getByIdentity(message.getFromIdentity());
  1211. if (messageModel == null) {
  1212. ContactMessageReceiver r = this.contactService.createReceiver(contactModel);
  1213. messageModel = r.createLocalModel(MessageType.TEXT, MessageContentsType.TEXT, message.getDate());
  1214. this.cache(messageModel);
  1215. messageModel.setApiMessageId(message.getMessageId().toString());
  1216. messageModel.setMessageFlags(message.getMessageFlags());
  1217. messageModel.setOutbox(false);
  1218. // replace CR by LF for Window$ Phone compatibility - me be removed soon.
  1219. String body = message.getText() != null ? message.getText().replace("\r", "\n") : null;
  1220. messageModel.setBodyAndQuotedMessageId(body);
  1221. messageModel.setIdentity(contactModel.getIdentity());
  1222. messageModel.setSaved(true);
  1223. this.databaseServiceNew.getMessageModelFactory().create(messageModel);
  1224. this.fireOnNewMessage(messageModel);
  1225. }
  1226. return messageModel;
  1227. }
  1228. private MessageModel saveBoxMessage(BallotCreateMessage message, MessageModel messageModel) throws Exception {
  1229. ContactModel contactModel = this.contactService.getByIdentity(message.getBallotCreator());
  1230. if(contactModel == null) {
  1231. return null;
  1232. }
  1233. MessageReceiver messageReceiver = this.contactService.createReceiver(contactModel);
  1234. return (MessageModel)this.saveBallotCreateMessage(
  1235. messageReceiver,
  1236. message.getMessageId(),
  1237. message,
  1238. messageModel);
  1239. }
  1240. private GroupMessageModel saveGroupMessage(GroupBallotCreateMessage message, GroupMessageModel messageModel) throws Exception {
  1241. GroupModel groupModel = this.groupService.getGroup(message);
  1242. if(groupModel == null) {
  1243. return null;
  1244. }
  1245. MessageReceiver messageReceiver = this.groupService.createReceiver(groupModel);
  1246. return (GroupMessageModel)this.saveBallotCreateMessage(
  1247. messageReceiver,
  1248. message.getMessageId(),
  1249. message,
  1250. messageModel);
  1251. }
  1252. private AbstractMessageModel saveBallotCreateMessage(MessageReceiver receiver,
  1253. MessageId messageId,
  1254. BallotCreateInterface message,
  1255. AbstractMessageModel messageModel)
  1256. throws ThreemaException
  1257. {
  1258. BallotUpdateResult result = this.ballotService.update(message);
  1259. if(result == null || result.getBallotModel() == null) {
  1260. throw new ThreemaException("could not create ballot model");
  1261. }
  1262. switch (result.getOperation()) {
  1263. case CREATE:
  1264. case CLOSE:
  1265. messageModel = this.createNewBallotMessage(
  1266. messageId,
  1267. result.getBallotModel(),
  1268. (result.getOperation() == BallotUpdateResult.Operation.CREATE ?
  1269. BallotDataModel.Type.BALLOT_CREATED:
  1270. BallotDataModel.Type.BALLOT_CLOSED),
  1271. receiver);
  1272. }
  1273. return messageModel;
  1274. }
  1275. private MessageModel saveBoxMessage(FileMessage message, MessageModel messageModel) throws Exception {
  1276. ContactModel contactModel = this.contactService.getByIdentity(message.getFromIdentity());
  1277. if(contactModel == null) {
  1278. logger.error("could not save a file message from an unknown contact");
  1279. return null;
  1280. }
  1281. MessageReceiver messageReceiver = this.contactService.createReceiver(contactModel);
  1282. return (MessageModel)this.saveFileMessage(
  1283. messageReceiver,
  1284. message,
  1285. messageModel);
  1286. }
  1287. private GroupMessageModel saveGroupMessage(GroupFileMessage message, GroupMessageModel messageModel) throws Exception {
  1288. GroupModel groupModel = this.groupService.getGroup(message);
  1289. if(groupModel == null) {
  1290. return null;
  1291. }
  1292. MessageReceiver messageReceiver = this.groupService.createReceiver(groupModel);
  1293. return (GroupMessageModel)this.saveFileMessage(
  1294. messageReceiver,
  1295. message,
  1296. messageModel);
  1297. }
  1298. private AbstractMessageModel saveAudioMessage(@NonNull MessageReceiver receiver,
  1299. AbstractMessage message,
  1300. AbstractMessageModel messageModel) throws Exception {
  1301. boolean newModel = false;
  1302. int duration;
  1303. byte[] encryptionKey, audioBlobId;
  1304. if (message instanceof GroupAudioMessage) {
  1305. duration = ((GroupAudioMessage) message).getDuration();
  1306. encryptionKey = ((GroupAudioMessage) message).getEncryptionKey();
  1307. audioBlobId = ((GroupAudioMessage) message).getAudioBlobId();
  1308. } else if (message instanceof BoxAudioMessage) {
  1309. duration = ((BoxAudioMessage) message).getDuration();
  1310. encryptionKey = ((BoxAudioMessage) message).getEncryptionKey();
  1311. audioBlobId = ((BoxAudioMessage) message).getAudioBlobId();
  1312. } else {
  1313. return null;
  1314. }
  1315. if (messageModel == null) {
  1316. newModel = true;
  1317. messageModel = receiver.createLocalModel(MessageType.VOICEMESSAGE, MessageContentsType.VOICE_MESSAGE, message.getDate());
  1318. this.cache(messageModel);
  1319. messageModel.setApiMessageId(message.getMessageId().toString());
  1320. messageModel.setMessageFlags(message.getMessageFlags());
  1321. messageModel.setOutbox(false);
  1322. messageModel.setIdentity(message.getFromIdentity());
  1323. messageModel.setAudioData(new AudioDataModel(duration, audioBlobId, encryptionKey));
  1324. //create the record
  1325. receiver.saveLocalModel(messageModel);
  1326. }
  1327. messageModel.setSaved(true);
  1328. receiver.saveLocalModel(messageModel);
  1329. if (newModel) {
  1330. this.fireOnCreatedMessage(messageModel);
  1331. if (canDownload(MessageType.VOICEMESSAGE)) {
  1332. downloadMediaMessage(messageModel, null);
  1333. }
  1334. }
  1335. else {
  1336. this.fireOnModifiedMessage(messageModel);
  1337. }
  1338. return messageModel;
  1339. }
  1340. private AbstractMessageModel saveVideoMessage(@NonNull MessageReceiver receiver,
  1341. AbstractMessage message,
  1342. AbstractMessageModel messageModel) throws Exception {
  1343. boolean newModel = false;
  1344. int duration, videoSize;
  1345. byte[] encryptionKey, videoBlobId, thumbnailBlobId;
  1346. if (message instanceof GroupVideoMessage) {
  1347. duration = ((GroupVideoMessage) message).getDuration();
  1348. videoSize = ((GroupVideoMessage) message).getVideoSize();
  1349. encryptionKey = ((GroupVideoMessage) message).getEncryptionKey();
  1350. videoBlobId = ((GroupVideoMessage) message).getVideoBlobId();
  1351. thumbnailBlobId = ((GroupVideoMessage) message).getThumbnailBlobId();
  1352. } else if (message instanceof BoxVideoMessage) {
  1353. duration = ((BoxVideoMessage) message).getDuration();
  1354. videoSize = ((BoxVideoMessage) message).getVideoSize();
  1355. encryptionKey = ((BoxVideoMessage) message).getEncryptionKey();
  1356. videoBlobId = ((BoxVideoMessage) message).getVideoBlobId();
  1357. thumbnailBlobId = ((BoxVideoMessage) message).getThumbnailBlobId();
  1358. } else {
  1359. return null;
  1360. }
  1361. if (messageModel == null) {
  1362. newModel = true;
  1363. messageModel = receiver.createLocalModel(MessageType.VIDEO, MessageContentsType.VIDEO, message.getDate());
  1364. this.cache(messageModel);
  1365. messageModel.setApiMessageId(message.getMessageId().toString());
  1366. messageModel.setMessageFlags(message.getMessageFlags());
  1367. messageModel.setOutbox(false);
  1368. messageModel.setIdentity(message.getFromIdentity());
  1369. messageModel.setVideoData(new VideoDataModel(duration, videoSize, videoBlobId, encryptionKey));
  1370. //create the record
  1371. receiver.saveLocalModel(messageModel);
  1372. }
  1373. //download thumbnail
  1374. final AbstractMessageModel messageModel1 = messageModel;
  1375. //use download service!
  1376. logger.info("Downloading blob for message {} id = {}", messageModel.getApiMessageId(), messageModel.getId());
  1377. byte[] thumbnailBlob = this.downloadService.download(
  1378. messageModel.getId(),
  1379. thumbnailBlobId,
  1380. !(message instanceof AbstractGroupMessage),
  1381. new ProgressListener() {
  1382. @Override
  1383. public void updateProgress(int progress) {
  1384. updateMessageLoadingProgress(messageModel1, progress);
  1385. }
  1386. @Override
  1387. public void onFinished(boolean success) {
  1388. setMessageLoadingFinished(messageModel1, success);
  1389. }
  1390. });
  1391. if (thumbnailBlob != null && thumbnailBlob.length > NaCl.BOXOVERHEAD) {
  1392. byte[] thumbnail = NaCl.symmetricDecryptData(thumbnailBlob, encryptionKey, ProtocolDefines.THUMBNAIL_NONCE);
  1393. try {
  1394. fileService.writeConversationMediaThumbnail(messageModel, thumbnail);
  1395. } catch (Exception e) {
  1396. this.downloadService.error(messageModel.getId());
  1397. throw e;
  1398. }
  1399. messageModel.setSaved(true);
  1400. receiver.saveLocalModel(messageModel);
  1401. this.downloadService.complete(messageModel.getId(), thumbnailBlobId);
  1402. if (newModel) {
  1403. this.fireOnCreatedMessage(messageModel);
  1404. if (canDownload(MessageType.VIDEO)) {
  1405. if (videoSize <= FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO) {
  1406. downloadMediaMessage(messageModel, null);
  1407. }
  1408. }
  1409. } else {
  1410. this.fireOnModifiedMessage(messageModel);
  1411. }
  1412. return messageModel;
  1413. }
  1414. this.downloadService.error(messageModel.getId());
  1415. return null;
  1416. }
  1417. private AbstractMessageModel saveFileMessage(MessageReceiver receiver,
  1418. AbstractMessage message,
  1419. AbstractMessageModel messageModel) throws Exception {
  1420. boolean newModel = false;
  1421. if(!(message instanceof FileMessageInterface)) {
  1422. throw new ThreemaException("not a file message interface");
  1423. }
  1424. FileData fileData = ((FileMessageInterface)message).getData();
  1425. if(null == fileData) {
  1426. return null;
  1427. }
  1428. if (TestUtil.empty(fileData.getMimeType())) {
  1429. fileData.setMimeType(MimeUtil.MIME_TYPE_DEFAULT);
  1430. }
  1431. logger.debug("process incoming file");
  1432. if (messageModel == null) {
  1433. newModel = true;
  1434. messageModel = receiver.createLocalModel(MessageType.FILE, MimeUtil.getContentTypeFromMimeType(fileData.getMimeType()), message.getDate());
  1435. this.cache(messageModel);
  1436. messageModel.setApiMessageId(message.getMessageId().toString());
  1437. messageModel.setMessageFlags(message.getMessageFlags());
  1438. messageModel.setOutbox(false);
  1439. messageModel.setIdentity(message.getFromIdentity());
  1440. // Save correlation id into db field instead json
  1441. messageModel.setCorrelationId(fileData.getCorrelationId());
  1442. messageModel.setFileData(
  1443. new FileDataModel(
  1444. fileData.getFileBlobId(),
  1445. fileData.getEncryptionKey(),
  1446. fileData.getMimeType(),
  1447. fileData.getThumbnailMimeType(),
  1448. fileData.getFileSize(),
  1449. FileUtil.sanitizeFileName(fileData.getFileName()),
  1450. fileData.getRenderingType(),
  1451. fileData.getDescription(),
  1452. false,
  1453. fileData.getMetaData()));
  1454. //create the record
  1455. receiver.saveLocalModel(messageModel);
  1456. }
  1457. boolean hasThumbnail = fileData.getThumbnailBlobId() != null;
  1458. if(hasThumbnail) {
  1459. logger.info("Downloading thumbnail of message " + message.getMessageId());
  1460. //download thumbnail
  1461. final AbstractMessageModel messageModel1 = messageModel;
  1462. //use download service!
  1463. byte[] thumbnailBlob = this.downloadService.download(
  1464. messageModel.getId(),
  1465. fileData.getThumbnailBlobId(),
  1466. !(message instanceof AbstractGroupMessage),
  1467. new ProgressListener() {
  1468. @Override
  1469. public void updateProgress(int progress) {
  1470. updateMessageLoadingProgress(messageModel1, progress);
  1471. }
  1472. @Override
  1473. public void onFinished(boolean success) {
  1474. setMessageLoadingFinished(messageModel1, success);
  1475. }
  1476. });
  1477. byte[] thumbnail = NaCl.symmetricDecryptData(thumbnailBlob, fileData.getEncryptionKey(), ProtocolDefines.FILE_THUMBNAIL_NONCE);
  1478. try {
  1479. fileService.writeConversationMediaThumbnail(messageModel, thumbnail);
  1480. } catch (Exception e) {
  1481. downloadService.error(messageModel.getId());
  1482. logger.info("Error writing thumbnail for message " + message.getMessageId());
  1483. throw e;
  1484. }
  1485. this.downloadService.complete(messageModel.getId(), fileData.getThumbnailBlobId());
  1486. }
  1487. messageModel.setSaved(true);
  1488. receiver.saveLocalModel(messageModel);
  1489. if(newModel) {
  1490. this.fireOnCreatedMessage(messageModel);
  1491. if (canDownload(messageModel)) {
  1492. if (fileData.getFileSize() <= FILE_AUTO_DOWNLOAD_MAX_SIZE_ISO) {
  1493. downloadMediaMessage(messageModel, null);
  1494. }
  1495. }
  1496. }
  1497. else {
  1498. this.fireOnModifiedMessage(messageModel);
  1499. }
  1500. return messageModel;
  1501. }
  1502. private GroupMessageModel saveGroupMessage(GroupTextMessage message, GroupMessageModel messageModel) throws Exception {
  1503. GroupModel groupModel = this.groupService.getGroup(message);
  1504. if(groupModel == null) {
  1505. return null;
  1506. }
  1507. if (messageModel == null) {
  1508. GroupMessageReceiver r = this.groupService.createReceiver(groupModel);
  1509. messageModel = r.createLocalModel(MessageType.TEXT, MessageContentsType.TEXT, message.getDate());
  1510. this.cache(messageModel);
  1511. messageModel.setApiMessageId(message.getMessageId().toString());
  1512. messageModel.setMessageFlags(message.getMessageFlags());
  1513. messageModel.setOutbox(false);
  1514. // replace CR by LF for Window$ Phone compatibility - me be removed soon.
  1515. String body = message.getText() != null ? message.getText().replace("\r", "\n") : null;
  1516. messageModel.setBodyAndQuotedMessageId(body);
  1517. messageModel.setSaved(true);
  1518. messageModel.setIdentity(message.getFromIdentity());
  1519. r.saveLocalModel(messageModel);
  1520. this.fireOnNewMessage(messageModel);
  1521. }
  1522. return messageModel;
  1523. }
  1524. private boolean canDownload(MessageType type) {
  1525. if (preferenceService != null) {
  1526. ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1527. NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
  1528. if (activeNetwork != null) {
  1529. switch (activeNetwork.getType()) {
  1530. case ConnectivityManager.TYPE_ETHERNET:
  1531. // fallthrough
  1532. case ConnectivityManager.TYPE_WIFI:
  1533. return preferenceService.getWifiAutoDownload().contains(String.valueOf(type.ordinal()));
  1534. case ConnectivityManager.TYPE_MOBILE:
  1535. return preferenceService.getMobileAutoDownload().contains(String.valueOf(type.ordinal()));
  1536. default:
  1537. break;
  1538. }
  1539. }
  1540. }
  1541. return false;
  1542. }
  1543. private boolean canDownload(@NonNull AbstractMessageModel messageModel) {
  1544. MessageType type = MessageType.FILE;
  1545. if (messageModel.getFileData() != null && messageModel.getFileData().getRenderingType() != FileData.RENDERING_DEFAULT) {
  1546. // treat media with default (file) rendering like a file for the sake of auto-download
  1547. if (messageModel.getMessageContentsType() == MessageContentsType.IMAGE) {
  1548. type = MessageType.IMAGE;
  1549. } else if (messageModel.getMessageContentsType() == MessageContentsType.VIDEO) {
  1550. type = MessageType.VIDEO;
  1551. }
  1552. }
  1553. if (preferenceService != null) {
  1554. ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
  1555. NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
  1556. if (activeNetwork != null) {
  1557. switch (activeNetwork.getType()) {
  1558. case ConnectivityManager.TYPE_ETHERNET:
  1559. // fallthrough
  1560. case ConnectivityManager.TYPE_WIFI:
  1561. return preferenceService.getWifiAutoDownload().contains(String.valueOf(type.ordinal()));
  1562. case ConnectivityManager.TYPE_MOBILE:
  1563. return preferenceService.getMobileAutoDownload().contains(String.valueOf(type.ordinal()));
  1564. default:
  1565. break;
  1566. }
  1567. }
  1568. }
  1569. return false;
  1570. }
  1571. private GroupMessageModel saveGroupMessage(GroupImageMessage message, GroupMessageModel messageModel) throws Exception {
  1572. GroupModel groupModel = this.groupService.getGroup(message);
  1573. if(groupModel == null) {
  1574. return null;
  1575. }
  1576. GroupMessageModelFactory messageModelFactory = this.databaseServiceNew.getGroupMessageModelFactory();
  1577. //download thumbnail
  1578. if (messageModel == null) {
  1579. MessageReceiver r = this.groupService.createReceiver(groupModel);
  1580. messageModel = (GroupMessageModel)r.createLocalModel(MessageType.IMAGE, MessageContentsType.IMAGE, message.getDate());
  1581. this.cache(messageModel);
  1582. messageModel.setApiMessageId(message.getMessageId().toString());
  1583. messageModel.setMessageFlags(message.getMessageFlags());
  1584. messageModel.setOutbox(false);
  1585. messageModel.setIdentity(message.getFromIdentity());
  1586. messageModel.setImageData(new ImageDataModel(
  1587. message.getBlobId(),
  1588. message.getEncryptionKey(),
  1589. ProtocolDefines.IMAGE_NONCE
  1590. ));
  1591. // Mark as saved to show message without image e.g.
  1592. messageModel.setSaved(true);
  1593. r.saveLocalModel(messageModel);
  1594. }
  1595. this.fireOnNewMessage(messageModel);
  1596. final GroupMessageModel messageModel1 = messageModel;
  1597. if (canDownload(MessageType.IMAGE) && !messageModel.getImageData().isDownloaded()) {
  1598. byte[] blob = this.downloadService.download(messageModel.getId(), message.getBlobId(), false, new ProgressListener() {
  1599. // do we really need a progress listener for images?
  1600. @Override
  1601. public void updateProgress(int progress) {
  1602. updateMessageLoadingProgress(messageModel1, progress);
  1603. }
  1604. @Override
  1605. public void onFinished(boolean success) {
  1606. setMessageLoadingFinished(messageModel1, success);
  1607. }
  1608. });
  1609. if (blob != null && messageModel.getImageData().getEncryptionKey().length > 0) {
  1610. try {
  1611. blob = NaCl.symmetricDecryptData(blob,
  1612. messageModel.getImageData().getEncryptionKey(),
  1613. messageModel.getImageData().getNonce());
  1614. } catch (Exception e) {
  1615. blob = null;
  1616. logger.error("Exception", e);
  1617. }
  1618. if (blob != null && blob.length > 0) {
  1619. try {
  1620. if (saveStrippedImage(blob, messageModel)) {
  1621. messageModel.getImageData().isDownloaded(true);
  1622. messageModel.writeDataModelToBody();
  1623. messageModelFactory.update(messageModel);
  1624. this.fireOnModifiedMessage(messageModel);
  1625. this.downloadService.complete(messageModel.getId(), message.getBlobId());
  1626. return messageModel;
  1627. }
  1628. } catch (Exception e) {
  1629. logger.error("Image save failed", e);
  1630. }
  1631. } else {
  1632. logger.error("Invalid blob");
  1633. }
  1634. } else {
  1635. logger.error("Blob is null");
  1636. }
  1637. this.downloadService.error(messageModel.getId());
  1638. }
  1639. messageModel.setSaved(true);
  1640. messageModelFactory.update(messageModel);
  1641. // download failed...let adapter know
  1642. fireOnModifiedMessage(messageModel);
  1643. return messageModel;
  1644. }
  1645. private GroupMessageModel saveGroupMessage(GroupVideoMessage message, GroupMessageModel messageModel) throws Exception {
  1646. GroupModel groupModel = this.groupService.getGroup(message);
  1647. if(groupModel == null) {
  1648. logger.error("could not save a group message from an unknown group");
  1649. return null;
  1650. }
  1651. MessageReceiver messageReceiver = this.groupService.createReceiver(groupModel);
  1652. return (GroupMessageModel)this.saveVideoMessage(
  1653. messageReceiver,
  1654. message,
  1655. messageModel);
  1656. }
  1657. private GroupMessageModel saveGroupMessage(GroupAudioMessage message, GroupMessageModel messageModel) throws Exception {
  1658. GroupModel groupModel = this.groupService.getGroup(message);
  1659. if(groupModel == null) {
  1660. return null;
  1661. }
  1662. MessageReceiver messageReceiver = this.groupService.createReceiver(groupModel);
  1663. return (GroupMessageModel)this.saveAudioMessage(
  1664. messageReceiver,
  1665. message,
  1666. messageModel);
  1667. }
  1668. private GroupMessageModel saveGroupMessage(GroupLocationMessage message, GroupMessageModel messageModel) throws SQLException {
  1669. GroupModel groupModel = this.groupService.getGroup(message);
  1670. boolean isNewMessage = false;
  1671. if(groupModel == null) {
  1672. return null;
  1673. }
  1674. MessageReceiver r = this.groupService.createReceiver(groupModel);
  1675. if (messageModel == null) {
  1676. messageModel = (GroupMessageModel)r.createLocalModel(MessageType.LOCATION, MessageContentsType.LOCATION, message.getDate());
  1677. this.cache(messageModel);
  1678. messageModel.setApiMessageId(message.getMessageId().toString());
  1679. messageModel.setMessageFlags(message.getMessageFlags());
  1680. messageModel.setOutbox(false);
  1681. messageModel.setIdentity(message.getFromIdentity());
  1682. r.saveLocalModel(messageModel);
  1683. isNewMessage = true;
  1684. }
  1685. String address = null;
  1686. try {
  1687. address = GeoLocationUtil.getAddressFromLocation(context, message.getLatitude(), message.getLongitude());
  1688. } catch (IOException e) {
  1689. logger.error("Exception", e);
  1690. //do not show this error!
  1691. }
  1692. messageModel.setLocationData(new LocationDataModel(
  1693. message.getLatitude(),
  1694. message.getLongitude(),
  1695. (long) message.getAccuracy(),
  1696. address,
  1697. message.getPoiName()
  1698. ));
  1699. messageModel.setSaved(true);
  1700. r.saveLocalModel(messageModel);
  1701. if(isNewMessage) {
  1702. this.fireOnNewMessage(messageModel);
  1703. }
  1704. else {
  1705. this.fireOnModifiedMessage(messageModel);
  1706. }
  1707. return messageModel;
  1708. }
  1709. private MessageModel saveBoxMessage(BoxImageMessage message, MessageModel messageModel) throws Exception {
  1710. logger.info("saveBoxMessage: {}", message.getMessageId());
  1711. ContactModel contactModel = this.contactService.getByIdentity(message.getFromIdentity());
  1712. logger.info("saveBoxMessage: {} - A", message.getMessageId());
  1713. MessageModelFactory messageModelFactory = this.databaseServiceNew.getMessageModelFactory();
  1714. logger.info("saveBoxMessage: {} - B", message.getMessageId());
  1715. if (messageModel == null) {
  1716. ContactMessageReceiver r = this.contactService.createReceiver(contactModel);
  1717. logger.info("saveBoxMessage: {} - C", message.getMessageId());
  1718. messageModel = r.createLocalModel(MessageType.IMAGE, MessageContentsType.IMAGE, message.getDate());
  1719. logger.info("saveBoxMessage: {} - D", message.getMessageId());
  1720. messageModel.setApiMessageId(message.getMessageId().toString());
  1721. messageModel.setMessageFlags(message.getMessageFlags());
  1722. messageModel.setOutbox(false);
  1723. messageModel.setIdentity(contactModel.getIdentity());
  1724. // Do not set an encryption key (asymmetric style)
  1725. messageModel.setImageData(new ImageDataModel(message.getBlobId(), contactModel.getPublicKey(), message.getNonce()));
  1726. // Mark as saved to show message without image e.g.
  1727. messageModel.setSaved(true);
  1728. r.saveLocalModel(messageModel);
  1729. /*
  1730. //create the record
  1731. messageModelFactory.create(messageModel);
  1732. */
  1733. logger.info("saveBoxMessage: {} - E", message.getMessageId());
  1734. this.cache(messageModel);
  1735. }
  1736. this.fireOnNewMessage(messageModel);
  1737. logger.info("saveBoxMessage: {} - F", message.getMessageId());
  1738. if (canDownload(MessageType.IMAGE) && !messageModel.getImageData().isDownloaded()) {
  1739. // Use download class to handle failures after downloads
  1740. byte[] imageBlob = this.downloadService.download(messageModel.getId(), message.getBlobId(), true, null);
  1741. if (imageBlob != null) {
  1742. byte[] image = identityStore.decryptData(imageBlob, message.getNonce(), contactModel.getPublicKey());
  1743. if (image != null) {
  1744. try {
  1745. if (saveStrippedImage(image, messageModel)) {
  1746. // Mark as downloaded
  1747. messageModel.getImageData().isDownloaded(true);
  1748. messageModel.writeDataModelToBody();
  1749. messageModelFactory.update(messageModel);
  1750. //fire on new
  1751. this.fireOnModifiedMessage(messageModel);
  1752. // remove blob
  1753. this.downloadService.complete(messageModel.getId(), message.getBlobId());
  1754. return messageModel;
  1755. }
  1756. } catch (Exception e) {
  1757. logger.error("Image save failed", e);
  1758. }
  1759. } else {
  1760. logger.error("Unable to decrypt blob for message {}", messageModel.getId());
  1761. }
  1762. } else {
  1763. logger.error("Blob is null");
  1764. }
  1765. this.downloadService.error(messageModel.getId());
  1766. }
  1767. messageModel.setSaved(true);
  1768. messageModelFactory.update(messageModel);
  1769. // download failed...let adapter know
  1770. fireOnModifiedMessage(messageModel);
  1771. return messageModel;
  1772. }
  1773. private MessageModel saveBoxMessage(BoxVideoMessage message, MessageModel messageModel) throws Exception {
  1774. ContactModel contactModel = this.contactService.getByIdentity(message.getFromIdentity());
  1775. if (contactModel == null) {
  1776. logger.error("could not save a video message from a unknown contact");
  1777. return null;
  1778. }
  1779. MessageReceiver messageReceiver = this.contactService.createReceiver(contactModel);
  1780. return (MessageModel)this.saveVideoMessage(
  1781. messageReceiver,
  1782. message,
  1783. messageModel);
  1784. }
  1785. private MessageModel saveBoxMessage(BoxAudioMessage message, MessageModel messageModel) throws Exception {
  1786. ContactModel contactModel = this.contactService.getByIdentity(message.getFromIdentity());
  1787. if (contactModel == null) {
  1788. logger.error("could not save an audio message from a unknown contact");
  1789. return null;
  1790. }
  1791. MessageReceiver messageReceiver = this.contactService.createReceiver(contactModel);
  1792. return (MessageModel)this.saveAudioMessage(
  1793. messageReceiver,
  1794. message,
  1795. messageModel);
  1796. }
  1797. private boolean saveStrippedImage(byte[] image, AbstractMessageModel messageModel) throws Exception {
  1798. boolean success = true;
  1799. // extract caption from exif data and strip all metadata, if any
  1800. try (ByteArrayOutputStream strippedImageOS = new ByteArrayOutputStream()) {
  1801. try (ByteArrayInputStream originalImageIS = new ByteArrayInputStream(image)) {
  1802. ExifInterface originalImageExif = new ExifInterface(originalImageIS);
  1803. String caption = originalImageExif.getUTF8StringAttribute(ExifInterface.TAG_ARTIST);
  1804. if (TestUtil.empty(caption)) {
  1805. caption = originalImageExif.getUTF8StringAttribute(ExifInterface.TAG_USER_COMMENT);
  1806. }
  1807. if (!TestUtil.empty(caption)) {
  1808. // strip trailing zero character from EXIF, if any
  1809. if (caption.charAt(caption.length() - 1) == '\u0000') {
  1810. caption = caption.substring(0, caption.length() - 1);
  1811. }
  1812. messageModel.setCaption(caption);
  1813. }
  1814. originalImageIS.reset();
  1815. // strip all exif data while saving
  1816. originalImageExif.saveAttributes(originalImageIS, strippedImageOS, true);
  1817. } catch (IOException e) {
  1818. logger.error("Exception", e);
  1819. success = false;
  1820. }
  1821. // check if a file already exist
  1822. fileService.removeMessageFiles(messageModel, true);
  1823. logger.info("Writing image file...");
  1824. if (success) {
  1825. // write stripped file
  1826. success = fileService.writeConversationMedia(messageModel, strippedImageOS.toByteArray());
  1827. } else {
  1828. // write original file
  1829. success = fileService.writeConversationMedia(messageModel, image);
  1830. }
  1831. if (success) {
  1832. logger.info("Image file successfully saved.");
  1833. } else {
  1834. logger.error("Image file save failed.");
  1835. }
  1836. messageModel.setSaved(true);
  1837. }
  1838. return success;
  1839. }
  1840. private MessageModel saveBoxMessage(BoxLocationMessage message, MessageModel messageModel) {
  1841. ContactModel contactModel = this.contactService.getByIdentity(message.getFromIdentity());
  1842. ContactMessageReceiver r = this.contactService.createReceiver(contactModel);
  1843. if (messageModel == null) {
  1844. messageModel = r.createLocalModel(MessageType.LOCATION, MessageContentsType.LOCATION, message.getDate());
  1845. this.cache(messageModel);
  1846. messageModel.setApiMessageId(message.getMessageId().toString());
  1847. messageModel.setMessageFlags(message.getMessageFlags());
  1848. messageModel.setOutbox(false);
  1849. }
  1850. String address = null;
  1851. try {
  1852. address = GeoLocationUtil.getAddressFromLocation(context, message.getLatitude(), message.getLongitude());
  1853. } catch (IOException e) {
  1854. logger.error("Exception", e);
  1855. //do not show this error!
  1856. }
  1857. messageModel.setLocationData(new LocationDataModel(
  1858. message.getLatitude(),
  1859. message.getLongitude(),
  1860. (long) message.getAccuracy(),
  1861. address,
  1862. message.getPoiName()
  1863. ));
  1864. messageModel.setIdentity(contactModel.getIdentity());
  1865. messageModel.setSaved(true);
  1866. //create the record
  1867. this.databaseServiceNew.getMessageModelFactory().create(messageModel);
  1868. this.fireOnNewMessage(messageModel);
  1869. return messageModel;
  1870. }
  1871. @Override
  1872. public List<AbstractMessageModel> getMessagesForReceiver(MessageReceiver receiver, MessageFilter messageFilter) {
  1873. return this.getMessagesForReceiver(receiver, messageFilter, true);
  1874. }
  1875. @Override
  1876. public List<AbstractMessageModel> getMessagesForReceiver(MessageReceiver receiver, MessageFilter messageFilter, boolean appendUnreadMessage) {
  1877. try {
  1878. List<AbstractMessageModel> messages = receiver.loadMessages(messageFilter);
  1879. if (!appendUnreadMessage) {
  1880. return messages;
  1881. }
  1882. switch (receiver.getType()) {
  1883. case MessageReceiver.Type_GROUP:
  1884. return this.markFirstUnread(messages);
  1885. case Type_CONTACT:
  1886. return this.markFirstUnread(messages);
  1887. default:
  1888. return messages;
  1889. }
  1890. } catch (SQLException e) {
  1891. logger.error("Exception", e);
  1892. }
  1893. return null;
  1894. }
  1895. /**
  1896. * Mark the first unread Message
  1897. *
  1898. * @param messageModels Message Models
  1899. * @return
  1900. */
  1901. private List<AbstractMessageModel> markFirstUnread(List<AbstractMessageModel> messageModels) {
  1902. synchronized (messageModels) {
  1903. int firstUnreadMessagePosition = -1;
  1904. for(int n = 0; n < messageModels.size(); n++) {
  1905. AbstractMessageModel m = messageModels.get(n);
  1906. if(m != null) {
  1907. if(m.isOutbox()) {
  1908. break;
  1909. }
  1910. else {
  1911. if(m.isRead()) {
  1912. break;
  1913. }
  1914. else if(!m.isStatusMessage()) {
  1915. firstUnreadMessagePosition = n;
  1916. }
  1917. }
  1918. }
  1919. }
  1920. if(firstUnreadMessagePosition > -1) {
  1921. FirstUnreadMessageModel firstUnreadMessageModel = new FirstUnreadMessageModel();
  1922. firstUnreadMessageModel.setCreatedAt(messageModels.get(firstUnreadMessagePosition).getCreatedAt());
  1923. messageModels.add(firstUnreadMessagePosition+1, firstUnreadMessageModel);
  1924. }
  1925. }
  1926. return messageModels;
  1927. }
  1928. @Override
  1929. public List<AbstractMessageModel> getMessagesForReceiver(MessageReceiver receiver) {
  1930. return this.getMessagesForReceiver(receiver, null);
  1931. }
  1932. @Override
  1933. public List<AbstractMessageModel> getMessageForBallot(final BallotModel ballotModel) {
  1934. try {
  1935. MessageReceiver receiver = this.ballotService.getReceiver(ballotModel);
  1936. if(receiver != null) {
  1937. List<AbstractMessageModel> ballotMessages = receiver.loadMessages(new MessageFilter() {
  1938. @Override
  1939. public long getPageSize() {
  1940. return 0;
  1941. }
  1942. @Override
  1943. public Integer getPageReferenceId() {
  1944. return null;
  1945. }
  1946. @Override
  1947. public boolean withStatusMessages() {
  1948. return false;
  1949. }
  1950. @Override
  1951. public boolean withUnsaved() {
  1952. return true;
  1953. }
  1954. @Override
  1955. public boolean onlyUnread() {
  1956. return false;
  1957. }
  1958. @Override
  1959. public boolean onlyDownloaded() {
  1960. return false;
  1961. }
  1962. @Override
  1963. public MessageType[] types() {
  1964. return new MessageType[]{
  1965. MessageType.BALLOT
  1966. };
  1967. }
  1968. @Override
  1969. public int[] contentTypes() {
  1970. return null;
  1971. }
  1972. });
  1973. return Functional.filter(ballotMessages, new IPredicateNonNull<AbstractMessageModel>() {
  1974. @Override
  1975. public boolean apply(@NonNull AbstractMessageModel type) {
  1976. return type.getBallotData() != null
  1977. && type.getBallotData().getBallotId() == ballotModel.getId();
  1978. }
  1979. });
  1980. }
  1981. } catch (SQLException e) {
  1982. logger.error("Exception", e);
  1983. }
  1984. return null;
  1985. }
  1986. @Override
  1987. public List<AbstractMessageModel> getContactMessagesForText(String query) {
  1988. return this.databaseServiceNew.getMessageModelFactory().getMessagesByText(query);
  1989. }
  1990. @Override
  1991. public List<AbstractMessageModel> getGroupMessagesForText(String query) {
  1992. return this.databaseServiceNew.getGroupMessageModelFactory().getMessagesByText(query);
  1993. }
  1994. private void readMessageQueue() {
  1995. try {
  1996. File f = this.getMessageQueueFile();
  1997. if (f.exists()) {
  1998. MasterKey masterKey = ThreemaApplication.getMasterKey();
  1999. if (masterKey == null || masterKey.isLocked())
  2000. return;
  2001. try (CipherInputStream cis = masterKey.getCipherInputStream(new FileInputStream(f))) {
  2002. messageQueue.unserializeFromStream(cis);
  2003. }
  2004. logger.info("Queue restored. Size = {}", messageQueue.getQueueSize());
  2005. }
  2006. } catch (Exception e) {
  2007. logger.error("Exception", e);
  2008. }
  2009. }
  2010. @Override
  2011. public void saveMessageQueue() {
  2012. MasterKey masterKey = ThreemaApplication.getMasterKey();
  2013. if (masterKey == null || masterKey.isLocked()) {
  2014. return;
  2015. }
  2016. new Thread(() -> {
  2017. synchronized (messageQueue) {
  2018. try {
  2019. FileOutputStream fileOutputStream = new FileOutputStream(getMessageQueueFile());
  2020. CipherOutputStream cos = masterKey.getCipherOutputStream(fileOutputStream);
  2021. if (cos != null) {
  2022. messageQueue.serializeToStream(cos);
  2023. logger.info("Queue saved. Size = {}", messageQueue.getQueueSize());
  2024. }
  2025. } catch (Exception e) {
  2026. logger.error("Exception", e);
  2027. }
  2028. }
  2029. });
  2030. }
  2031. @Override
  2032. public MessageModel getContactMessageModel(final Integer id, boolean lazy) {
  2033. MessageModel model;
  2034. synchronized (this.contactMessageCache) {
  2035. model = Functional.select(this.contactMessageCache, new IPredicateNonNull<MessageModel>() {
  2036. @Override
  2037. public boolean apply(@NonNull MessageModel type) {
  2038. return type.getId() == id;
  2039. }
  2040. });
  2041. }
  2042. if (lazy && model == null) {
  2043. model = this.databaseServiceNew.getMessageModelFactory().getById(id);
  2044. if (model != null) {
  2045. synchronized (this.contactMessageCache) {
  2046. this.contactMessageCache.add(model);
  2047. }
  2048. }
  2049. }
  2050. return model;
  2051. }
  2052. private MessageModel getContactMessageModel(@NonNull final String apiMessageId) {
  2053. MessageModel model;
  2054. synchronized (this.contactMessageCache) {
  2055. model = Functional.select(this.contactMessageCache, new IPredicateNonNull<MessageModel>() {
  2056. @Override
  2057. public boolean apply(@NonNull MessageModel messageModel) {
  2058. return apiMessageId.equals(messageModel.getApiMessageId());
  2059. }
  2060. });
  2061. }
  2062. if (model == null) {
  2063. try {
  2064. model = this.databaseServiceNew.getMessageModelFactory().getByApiMessageId(new MessageId(Utils.hexStringToByteArray(apiMessageId)));
  2065. if (model != null) {
  2066. synchronized (this.contactMessageCache) {
  2067. this.contactMessageCache.add(model);
  2068. }
  2069. }
  2070. }
  2071. catch (ThreemaException ignore) {}
  2072. }
  2073. return model;
  2074. }
  2075. @Override
  2076. public GroupMessageModel getGroupMessageModel(final Integer id, boolean lazy) {
  2077. synchronized (this.groupMessageCache) {
  2078. GroupMessageModel model = Functional.select(this.groupMessageCache, new IPredicateNonNull<GroupMessageModel>() {
  2079. @Override
  2080. public boolean apply(@NonNull GroupMessageModel type) {
  2081. return type.getId() == id;
  2082. }
  2083. });
  2084. if (lazy && model == null) {
  2085. model = this.databaseServiceNew.getGroupMessageModelFactory().getById(id);
  2086. if (model != null) {
  2087. this.groupMessageCache.add(model);
  2088. }
  2089. }
  2090. return model;
  2091. }
  2092. }
  2093. private GroupMessageModel getGroupMessageModel(@NonNull final String apiMessageId) {
  2094. synchronized (this.groupMessageCache) {
  2095. GroupMessageModel model = Functional.select(this.groupMessageCache, new IPredicateNonNull<GroupMessageModel>() {
  2096. @Override
  2097. public boolean apply(@NonNull GroupMessageModel messageModel) {
  2098. return apiMessageId.equals(messageModel.getApiMessageId());
  2099. }
  2100. });
  2101. if (model == null) {
  2102. try {
  2103. model = this.databaseServiceNew.getGroupMessageModelFactory().getByApiMessageId(new MessageId(Utils.hexStringToByteArray(apiMessageId)));
  2104. if (model != null) {
  2105. this.groupMessageCache.add(model);
  2106. }
  2107. }
  2108. catch (ThreemaException ignore) {}
  2109. }
  2110. return model;
  2111. }
  2112. }
  2113. @Override
  2114. public DistributionListMessageModel getDistributionListMessageModel(Integer id, boolean lazy) {
  2115. return this.databaseServiceNew.getDistributionListMessageModelFactory().getById(
  2116. id
  2117. );
  2118. }
  2119. private DistributionListMessageModel getDistributionListMessageModel(String apiMessageId) {
  2120. return this.databaseServiceNew.getDistributionListMessageModelFactory().getByApiMessageId(
  2121. apiMessageId
  2122. );
  2123. }
  2124. private void fireOnNewMessage(final AbstractMessageModel messageModel) {
  2125. if(this.appLockService.isLocked()) {
  2126. //do not fire messages, wait until app is unlocked
  2127. this.appLockService.addOnLockAppStateChanged(new LockAppService.OnLockAppStateChanged() {
  2128. @Override
  2129. public boolean changed(boolean locked) {
  2130. if(!locked) {
  2131. // fireOnNewMessage(messageModel);
  2132. return true;
  2133. }
  2134. return false;
  2135. }
  2136. });
  2137. }
  2138. this.fireOnCreatedMessage(messageModel);
  2139. }
  2140. @Override
  2141. public MessageString getMessageString(AbstractMessageModel messageModel, int maxLength) {
  2142. return getMessageString(messageModel, maxLength,true);
  2143. }
  2144. @NonNull
  2145. @Override
  2146. public MessageString getMessageString(AbstractMessageModel messageModel, int maxLength, boolean withPrefix) {
  2147. boolean isHidden = false;
  2148. String prefix = "";
  2149. if (messageModel instanceof GroupMessageModel) {
  2150. //append Username
  2151. if (withPrefix) {
  2152. prefix = NameUtil.getShortName(this.context, messageModel, this.contactService) + ": ";
  2153. }
  2154. final GroupModel groupModel = groupService.getById(((GroupMessageModel)messageModel).getGroupId());
  2155. isHidden = hiddenChatsListService.has(groupService.getUniqueIdString(groupModel));
  2156. } else {
  2157. final ContactModel contactModel = contactService.getByIdentity(messageModel.getIdentity());
  2158. isHidden = hiddenChatsListService.has(contactService.getUniqueIdString(contactModel));
  2159. }
  2160. if (isHidden) {
  2161. return new MessageString(context.getString(R.string.new_messages_locked));
  2162. }
  2163. switch (messageModel.getType()) {
  2164. case TEXT:
  2165. String messageText, rawMessageText;
  2166. messageText = QuoteUtil.getMessageBody(messageModel, false);
  2167. rawMessageText = prefix + messageText;
  2168. if ((maxLength > 0) && (messageText.length() > maxLength)) {
  2169. messageText = messageText.substring(0, maxLength - 3) + "...";
  2170. }
  2171. return new MessageString(messageText, rawMessageText);
  2172. case VIDEO:
  2173. return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder));
  2174. case LOCATION:
  2175. String locationString = prefix + context.getResources().getString(R.string.location_placeholder);
  2176. if (!TestUtil.empty(messageModel.getLocationData().getPoi())) {
  2177. locationString += ": " + messageModel.getLocationData().getPoi();
  2178. }
  2179. return new MessageString(locationString);
  2180. case VOICEMESSAGE:
  2181. String messageString = prefix + context.getResources().getString(R.string.audio_placeholder);
  2182. if (messageModel.getAudioData() != null) {
  2183. messageString += " (" + StringConversionUtil.secondsToString(messageModel.getAudioData().getDuration(), false) + ")";
  2184. }
  2185. return new MessageString(messageString);
  2186. case FILE:
  2187. if (MimeUtil.isImageFile(messageModel.getFileData().getMimeType())) {
  2188. if (TestUtil.empty(messageModel.getCaption())) {
  2189. return new MessageString(prefix + context.getResources().getString(R.string.image_placeholder));
  2190. } else {
  2191. return new MessageString(prefix + context.getResources().getString(R.string.image_placeholder) + ": " + messageModel.getFileData().getCaption());
  2192. }
  2193. } else if (MimeUtil.isVideoFile(messageModel.getFileData().getMimeType())) {
  2194. if (TestUtil.empty(messageModel.getFileData().getCaption())) {
  2195. String durationString = messageModel.getFileData().getDurationString();
  2196. if (durationString != null) {
  2197. return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder) + " (" + durationString + ")");
  2198. } else {
  2199. return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder));
  2200. }
  2201. } else {
  2202. return new MessageString(prefix + context.getResources().getString(R.string.video_placeholder) + ": " + messageModel.getFileData().getCaption());
  2203. }
  2204. } else if (MimeUtil.isAudioFile(messageModel.getFileData().getMimeType())) {
  2205. if (TestUtil.empty(messageModel.getFileData().getCaption())) {
  2206. String durationString = messageModel.getFileData().getDurationString();
  2207. if (durationString != null) {
  2208. return new MessageString(prefix + context.getResources().getString(R.string.audio_placeholder) + " (" + durationString + ")");
  2209. } else {
  2210. return new MessageString(prefix + context.getResources().getString(R.string.audio_placeholder));
  2211. }
  2212. } else {
  2213. return new MessageString(prefix + context.getResources().getString(R.string.audio_placeholder) + ": " + messageModel.getFileData().getCaption());
  2214. }
  2215. } else {
  2216. if (TestUtil.empty(messageModel.getFileData().getCaption())) {
  2217. return new MessageString(prefix + context.getResources().getString(R.string.file_placeholder) + ": " + messageModel.getFileData().getFileName());
  2218. } else {
  2219. return new MessageString(prefix + context.getResources().getString(R.string.file_placeholder) + ": " + messageModel.getFileData().getCaption());
  2220. }
  2221. }
  2222. case IMAGE:
  2223. if (TestUtil.empty(messageModel.getCaption())) {
  2224. return new MessageString(prefix + context.getResources().getString(R.string.image_placeholder));
  2225. } else {
  2226. return new MessageString(prefix + context.getResources().getString(R.string.image_placeholder) + ": " + messageModel.getCaption());
  2227. }
  2228. case BALLOT:
  2229. return new MessageString(prefix + context.getResources().getString(R.string.ballot_placeholder) + ":" + BallotUtil.getNotificationString(context, messageModel));
  2230. case VOIP_STATUS:
  2231. return new MessageString(prefix + MessageUtil.getViewElement(this.context, messageModel).placeholder);
  2232. default:
  2233. return new MessageString(prefix);
  2234. }
  2235. }
  2236. public void saveIncomingServerMessage(final ServerMessageModel msg) {
  2237. //do not save the server message model for this moment!
  2238. //show as alert
  2239. ListenerManager.serverMessageListeners.handle(new ListenerManager.HandleListener<ServerMessageListener>() {
  2240. @Override
  2241. public void handle(ServerMessageListener listener) {
  2242. if (msg.getType() == ServerMessageModel.Type.ALERT) {
  2243. listener.onAlert(msg);
  2244. } else {
  2245. listener.onError(msg);
  2246. }
  2247. }
  2248. });
  2249. }
  2250. @Override
  2251. public boolean downloadMediaMessage(AbstractMessageModel mediaMessageModel, ProgressListener progressListener) throws Exception {
  2252. //TODO: create messageutil can download file method and unit test
  2253. if (mediaMessageModel.getType() != MessageType.IMAGE
  2254. && mediaMessageModel.getType() != MessageType.VIDEO
  2255. && mediaMessageModel.getType() != MessageType.VOICEMESSAGE
  2256. && mediaMessageModel.getType() != MessageType.FILE) {
  2257. throw new ThreemaException("message is not a media message");
  2258. }
  2259. MediaMessageDataInterface data = null;
  2260. byte[] nonce = null;
  2261. if(mediaMessageModel.getType() == MessageType.IMAGE) {
  2262. data = mediaMessageModel.getImageData();
  2263. nonce = ProtocolDefines.IMAGE_NONCE;
  2264. }
  2265. if(mediaMessageModel.getType() == MessageType.VIDEO) {
  2266. data = mediaMessageModel.getVideoData();
  2267. nonce = ProtocolDefines.VIDEO_NONCE;
  2268. }
  2269. else if(mediaMessageModel.getType() == MessageType.VOICEMESSAGE) {
  2270. data = mediaMessageModel.getAudioData();
  2271. nonce = ProtocolDefines.AUDIO_NONCE;
  2272. }
  2273. else if(mediaMessageModel.getType() == MessageType.FILE) {
  2274. data = mediaMessageModel.getFileData();
  2275. nonce = ProtocolDefines.FILE_NONCE;
  2276. }
  2277. if (data != null && !data.isDownloaded()) {
  2278. byte[] blob = this.downloadService.download(
  2279. mediaMessageModel.getId(),
  2280. data.getBlobId(),
  2281. !(mediaMessageModel instanceof GroupMessageModel),
  2282. progressListener);
  2283. if (blob == null || blob.length < NaCl.BOXOVERHEAD) {
  2284. logger.error("Blob for message {} is empty", mediaMessageModel.getApiMessageId());
  2285. this.downloadService.error(mediaMessageModel.getId());
  2286. // blob download failed or empty or canceled
  2287. throw new ThreemaException("failed to download message");
  2288. }
  2289. boolean success = false;
  2290. if (mediaMessageModel.getType() != MessageType.IMAGE) {
  2291. logger.info("Decrypting blob for message {}", mediaMessageModel.getApiMessageId());
  2292. if (NaCl.symmetricDecryptDataInplace(blob, data.getEncryptionKey(), nonce)) {
  2293. logger.info("Write conversation media for message {}", mediaMessageModel.getApiMessageId());
  2294. // save the file
  2295. try {
  2296. if (this.fileService.writeConversationMedia(mediaMessageModel, blob, 0, blob.length - NaCl.BOXOVERHEAD, true)) {
  2297. success = true;
  2298. logger.info("Media for message {} successfully saved.", mediaMessageModel.getApiMessageId());
  2299. }
  2300. } catch (Exception e) {
  2301. logger.warn("Unable to save media");
  2302. this.downloadService.error(mediaMessageModel.getId());
  2303. throw new ThreemaException("Unable to save media");
  2304. }
  2305. }
  2306. } else {
  2307. byte image[];
  2308. if (mediaMessageModel instanceof GroupMessageModel) {
  2309. image = NaCl.symmetricDecryptData(blob, data.getEncryptionKey(), nonce);
  2310. } else {
  2311. image = identityStore.decryptData(blob, data.getNonce(), data.getEncryptionKey());
  2312. }
  2313. if (image != null && image.length > 0) {
  2314. try {
  2315. // save the file
  2316. success = saveStrippedImage(image, mediaMessageModel);
  2317. } catch (Exception e) {
  2318. logger.error("Exception", e);
  2319. }
  2320. }
  2321. }
  2322. if (success) {
  2323. data.isDownloaded(true);
  2324. if(mediaMessageModel.getType() == MessageType.IMAGE) {
  2325. mediaMessageModel.setImageData((ImageDataModel)data);
  2326. mediaMessageModel.writeDataModelToBody();
  2327. }
  2328. else if(mediaMessageModel.getType() == MessageType.VIDEO) {
  2329. mediaMessageModel.setVideoData((VideoDataModel)data);
  2330. }
  2331. else if(mediaMessageModel.getType() == MessageType.VOICEMESSAGE) {
  2332. mediaMessageModel.setAudioData((AudioDataModel) data);
  2333. }
  2334. else if(mediaMessageModel.getType() == MessageType.FILE) {
  2335. mediaMessageModel.setFileData((FileDataModel) data);
  2336. }
  2337. this.save(mediaMessageModel);
  2338. this.fireOnModifiedMessage(mediaMessageModel);
  2339. this.downloadService.complete(mediaMessageModel.getId(), data.getBlobId());
  2340. // silently save images and videos to gallery if enabled
  2341. if (preferenceService != null && preferenceService.isSaveMedia()) {
  2342. if (mediaMessageModel.getType() == MessageType.IMAGE ||
  2343. mediaMessageModel.getType() == MessageType.VIDEO ||
  2344. (mediaMessageModel.getType() == MessageType.FILE &&
  2345. (FileUtil.isImageFile((FileDataModel) data) ||
  2346. FileUtil.isVideoFile((FileDataModel) data)))) {
  2347. boolean isHidden;
  2348. if (mediaMessageModel instanceof GroupMessageModel) {
  2349. isHidden = hiddenChatsListService.has(groupService.getUniqueIdString(((GroupMessageModel) mediaMessageModel).getGroupId()));
  2350. } else {
  2351. isHidden = hiddenChatsListService.has(contactService.getUniqueIdString(mediaMessageModel.getIdentity()));
  2352. }
  2353. if (!isHidden) {
  2354. fileService.saveMedia(null, null, new CopyOnWriteArrayList<>(Collections.singletonList(mediaMessageModel)), true);
  2355. }
  2356. }
  2357. }
  2358. return true;
  2359. } else {
  2360. logger.error("Decryption failed");
  2361. this.downloadService.error(mediaMessageModel.getId());
  2362. throw new ThreemaException("Decryption failed");
  2363. }
  2364. }
  2365. return false;
  2366. }
  2367. @Override
  2368. public boolean cancelMessageDownload(AbstractMessageModel messageModel) {
  2369. return this.downloadService.cancel(messageModel.getId());
  2370. }
  2371. private void fireOnCreatedMessage(final AbstractMessageModel messageModel) {
  2372. logger.debug("fireOnCreatedMessage for message " + messageModel.getApiMessageId());
  2373. ListenerManager.messageListeners.handle(new ListenerManager.HandleListener<MessageListener>() {
  2374. @Override
  2375. public void handle(MessageListener listener) {
  2376. listener.onNew(messageModel);
  2377. }
  2378. });
  2379. }
  2380. private void fireOnModifiedMessage(final AbstractMessageModel messageModel) {
  2381. ListenerManager.messageListeners.handle(new ListenerManager.HandleListener<MessageListener>() {
  2382. @Override
  2383. public void handle(MessageListener listener) {
  2384. List<AbstractMessageModel> list = new ArrayList<>();
  2385. list.add(messageModel);
  2386. listener.onModified(list);
  2387. }
  2388. });
  2389. }
  2390. private void fireOnRemovedMessage(final AbstractMessageModel messageModel) {
  2391. ListenerManager.messageListeners.handle(new ListenerManager.HandleListener<MessageListener>() {
  2392. @Override
  2393. public void handle(MessageListener listener) {
  2394. listener.onRemoved(messageModel);
  2395. }
  2396. });
  2397. }
  2398. private void setMessageLoadingFinished(AbstractMessageModel messageModel, boolean success) {
  2399. this.loadingProgress.delete(messageModel.getId());
  2400. this.cancelUploader(messageModel);
  2401. }
  2402. private void updateMessageLoadingProgress(final AbstractMessageModel messageModel, final int progress) {
  2403. this.loadingProgress.put(messageModel.getId(), progress);
  2404. //handle progress
  2405. ListenerManager.messageListeners.handle(new ListenerManager.HandleListener<MessageListener>() {
  2406. @Override
  2407. public void handle(MessageListener listener) {
  2408. listener.onProgressChanged(messageModel, progress);
  2409. }
  2410. });
  2411. }
  2412. @Override
  2413. public void removeAll() throws SQLException, IOException, ThreemaException {
  2414. //use the fast way
  2415. this.databaseServiceNew.getMessageModelFactory().deleteAll();
  2416. this.databaseServiceNew.getGroupMessageModelFactory().deleteAll();
  2417. this.databaseServiceNew.getDistributionListMessageModelFactory().deleteAll();
  2418. //clear all caches
  2419. synchronized (this.contactMessageCache) {
  2420. this.contactMessageCache.clear();
  2421. }
  2422. //clear all caches
  2423. synchronized (this.groupMessageCache) {
  2424. this.groupMessageCache.clear();
  2425. }
  2426. //clear all caches
  2427. synchronized (this.distributionListMessageCache) {
  2428. this.distributionListMessageCache.clear();
  2429. }
  2430. //clear all files in app Path
  2431. this.fileService.clearDirectory(this.fileService.getAppDataPath(), false);
  2432. /* remove saved message queue too */
  2433. File f = new File(context.getFilesDir(), MESSAGE_QUEUE_SAVE_FILE);
  2434. if (f.exists()) {
  2435. FileUtil.deleteFileOrWarn(f, "message queue save file", logger);
  2436. }
  2437. }
  2438. @Override
  2439. public void save(final AbstractMessageModel messageModel) {
  2440. if(messageModel != null) {
  2441. if(messageModel instanceof MessageModel) {
  2442. synchronized (this.contactMessageCache) {
  2443. this.databaseServiceNew.getMessageModelFactory().createOrUpdate(
  2444. (MessageModel) messageModel
  2445. );
  2446. //remove "old" message models from cache
  2447. for(MessageModel m: Functional.filter(this.contactMessageCache, new IPredicateNonNull<MessageModel>() {
  2448. @Override
  2449. public boolean apply(@NonNull MessageModel type) {
  2450. return type.getId() == messageModel.getId() && messageModel != type;
  2451. }
  2452. })){
  2453. //remove cached unsaved object
  2454. logger.debug("copy from message model fix");
  2455. m.copyFrom(messageModel);
  2456. }
  2457. }
  2458. }
  2459. else if(messageModel instanceof GroupMessageModel) {
  2460. synchronized (this.groupMessageCache) {
  2461. this.databaseServiceNew.getGroupMessageModelFactory().createOrUpdate(
  2462. (GroupMessageModel) messageModel);
  2463. //remove "old" message models from cache
  2464. for(GroupMessageModel m: Functional.filter(this.groupMessageCache, new IPredicateNonNull<GroupMessageModel>() {
  2465. @Override
  2466. public boolean apply(@NonNull GroupMessageModel type) {
  2467. return type.getId() == messageModel.getId() && messageModel != type;
  2468. }
  2469. })){
  2470. //remove cached unsaved object
  2471. logger.debug("copy from group message model fix");
  2472. m.copyFrom(messageModel);
  2473. }
  2474. }
  2475. }
  2476. else if(messageModel instanceof DistributionListMessageModel) {
  2477. synchronized (this.distributionListMessageCache) {
  2478. this.databaseServiceNew.getDistributionListMessageModelFactory().createOrUpdate(
  2479. (DistributionListMessageModel) messageModel);
  2480. //remove "old" message models from cache
  2481. for(DistributionListMessageModel m: Functional.filter(this.distributionListMessageCache, new IPredicateNonNull<DistributionListMessageModel>() {
  2482. @Override
  2483. public boolean apply(@NonNull DistributionListMessageModel type) {
  2484. return type.getId() == messageModel.getId() && messageModel != type;
  2485. }
  2486. })){
  2487. //remove cached unsaved object
  2488. logger.debug("copy from distribution list message model fix");
  2489. m.copyFrom(messageModel);
  2490. }
  2491. }
  2492. }
  2493. // Cache the element for more actions
  2494. this.cache(messageModel);
  2495. }
  2496. }
  2497. @Override
  2498. public long getTotalMessageCount() {
  2499. //simple count
  2500. return this.databaseServiceNew.getMessageModelFactory().count()
  2501. + this.databaseServiceNew.getGroupMessageModelFactory().count()
  2502. + this.databaseServiceNew.getDistributionListMessageModelFactory().count();
  2503. }
  2504. @NonNull
  2505. private String getMimeTypeString(AbstractMessageModel model) {
  2506. switch (model.getType()) {
  2507. case VIDEO:
  2508. return MimeUtil.MIME_TYPE_VIDEO;
  2509. case FILE:
  2510. return model.getFileData().getMimeType();
  2511. case VOICEMESSAGE:
  2512. return MimeUtil.MIME_TYPE_AUDIO;
  2513. case IMAGE:
  2514. return MimeUtil.MIME_TYPE_IMAGE_JPG;
  2515. default:
  2516. return MimeUtil.MIME_TYPE_ANY;
  2517. }
  2518. }
  2519. private String getLeastCommonDenominatorMimeType(ArrayList<AbstractMessageModel> models) {
  2520. String mimeType = getMimeTypeString(models.get(0));
  2521. if (models.size() > 1) {
  2522. for (int i = 1; i < models.size(); i++) {
  2523. mimeType = MimeUtil.getCommonMimeType(mimeType, getMimeTypeString(models.get(i)));
  2524. }
  2525. }
  2526. return mimeType;
  2527. }
  2528. @Override
  2529. public boolean shareMediaMessages(final Context context, ArrayList<AbstractMessageModel> models, ArrayList<Uri> shareFileUris) {
  2530. if (TestUtil.required(context, models, shareFileUris)) {
  2531. if (models.size() > 0 && shareFileUris.size() > 0) {
  2532. Intent intent;
  2533. if (models.size() == 1) {
  2534. AbstractMessageModel model = models.get(0);
  2535. Uri shareFileUri = shareFileUris.get(0);
  2536. if (shareFileUri == null) {
  2537. logger.info("No file to share");
  2538. return false;
  2539. }
  2540. intent = new Intent(Intent.ACTION_SEND);
  2541. intent.putExtra(Intent.EXTRA_STREAM, shareFileUri);
  2542. intent.setType(getMimeTypeString(model));
  2543. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(shareFileUri.getScheme())) {
  2544. intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  2545. }
  2546. } else {
  2547. intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
  2548. intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, shareFileUris);
  2549. Uri firstShareFileUri = shareFileUris.get(0);
  2550. intent.setType(getLeastCommonDenominatorMimeType(models));
  2551. if (firstShareFileUri != null) {
  2552. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(firstShareFileUri.getScheme())) {
  2553. intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
  2554. }
  2555. }
  2556. }
  2557. try {
  2558. context.startActivity(Intent.createChooser(intent, context.getResources().getText(R.string.share_via)));
  2559. } catch (ActivityNotFoundException e) {
  2560. // make sure Toast runs in UI thread
  2561. RuntimeUtil.runOnUiThread(new Runnable() {
  2562. @Override
  2563. public void run() {
  2564. Toast.makeText(context, R.string.no_activity_for_mime_type, Toast.LENGTH_SHORT).show();
  2565. }
  2566. });
  2567. }
  2568. }
  2569. }
  2570. return false;
  2571. }
  2572. @Override
  2573. public boolean viewMediaMessage(final Context context, AbstractMessageModel model, Uri uri) {
  2574. if (TestUtil.required(context, model, uri)) {
  2575. Intent intent = new Intent(Intent.ACTION_VIEW);
  2576. String mimeType = getMimeTypeString(model);
  2577. if (MimeUtil.isImageFile(mimeType)) {
  2578. // some viewers cannot handle image/gif - give them a generic mime type
  2579. mimeType = MimeUtil.MIME_TYPE_IMAGE;
  2580. }
  2581. intent.setDataAndType(uri, mimeType);
  2582. if (ContentResolver.SCHEME_CONTENT.equalsIgnoreCase(uri.getScheme())) {
  2583. intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_ACTIVITY_NEW_TASK);
  2584. } else if (!(context instanceof Activity)) {
  2585. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  2586. }
  2587. try {
  2588. context.startActivity(intent);
  2589. } catch (ActivityNotFoundException e) {
  2590. // make sure Toast runs in UI thread
  2591. RuntimeUtil.runOnUiThread(new Runnable() {
  2592. @Override
  2593. public void run() {
  2594. Toast.makeText(context, R.string.no_activity_for_mime_type, Toast.LENGTH_SHORT).show();
  2595. }
  2596. });
  2597. }
  2598. }
  2599. return false;
  2600. }
  2601. @Override
  2602. public boolean shareTextMessage(Context context, AbstractMessageModel model) {
  2603. if (model != null) {
  2604. String text = "";
  2605. Intent intent = new Intent();
  2606. if (model.getType() == MessageType.LOCATION && model.getLocationData() != null) {
  2607. Uri locationUri = GeoLocationUtil.getLocationUri(model);
  2608. if (!TestUtil.empty(model.getLocationData().getAddress())) {
  2609. text = model.getLocationData().getAddress() + " - ";
  2610. }
  2611. text += locationUri.toString();
  2612. } else {
  2613. text = model.getBody();
  2614. }
  2615. intent.setAction(Intent.ACTION_SEND);
  2616. intent.putExtra(android.content.Intent.EXTRA_TEXT, text);
  2617. intent.setType("text/plain");
  2618. if (intent.resolveActivity(context.getPackageManager()) != null) {
  2619. context.startActivity(Intent.createChooser(intent, context.getResources().getText(R.string.share_via)));
  2620. }
  2621. }
  2622. return false;
  2623. }
  2624. private File getMessageQueueFile() {
  2625. return new File(context.getFilesDir(), MESSAGE_QUEUE_SAVE_FILE);
  2626. }
  2627. @Override
  2628. public void markConversationAsRead(MessageReceiver messageReceiver, NotificationService notificationService) {
  2629. try {
  2630. @SuppressWarnings("unchecked")
  2631. List<AbstractMessageModel> unreadMessages = messageReceiver.loadMessages(new MessageService.MessageFilter() {
  2632. @Override
  2633. public long getPageSize() {
  2634. return 0;
  2635. }
  2636. @Override
  2637. public Integer getPageReferenceId() {
  2638. return null;
  2639. }
  2640. @Override
  2641. public boolean withStatusMessages() {
  2642. return false;
  2643. }
  2644. @Override
  2645. public boolean withUnsaved() {
  2646. return false;
  2647. }
  2648. @Override
  2649. public boolean onlyUnread() {
  2650. return true;
  2651. }
  2652. @Override
  2653. public boolean onlyDownloaded() {
  2654. return false;
  2655. }
  2656. @Override
  2657. public MessageType[] types() {
  2658. return null;
  2659. }
  2660. @Override
  2661. public int[] contentTypes() {
  2662. return null;
  2663. }
  2664. });
  2665. if (unreadMessages != null && unreadMessages.size() > 0) {
  2666. //do not run on a own thread, create a new thread outside!
  2667. (new ReadMessagesRoutine(unreadMessages, this, notificationService)).run();
  2668. }
  2669. } catch (SQLException e) {
  2670. logger.error("Exception", e);
  2671. }
  2672. }
  2673. @Override
  2674. public void markMessageAsRead(AbstractMessageModel abstractMessageModel, NotificationService notificationService) {
  2675. List<AbstractMessageModel> messages = new ArrayList<>();
  2676. messages.add(abstractMessageModel);
  2677. new Thread(new ReadMessagesRoutine(messages, this, notificationService)).start();
  2678. }
  2679. @Override
  2680. public AbstractMessageModel getMessageModelFromId(int id, String type) {
  2681. if (id != 0 && !TestUtil.empty(type)) {
  2682. if (type.equals(MessageModel.class.toString())) {
  2683. return getContactMessageModel(id, true);
  2684. } else if (type.equals(GroupMessageModel.class.toString())) {
  2685. return getGroupMessageModel(id, true);
  2686. } else if (type.equals(DistributionListMessageModel.class.toString())) {
  2687. return getDistributionListMessageModel(id, true);
  2688. }
  2689. }
  2690. return null;
  2691. }
  2692. @Override
  2693. public AbstractMessageModel getMessageModelByApiMessageId(String apiMessageId, @MessageReceiver.MessageReceiverType int type) {
  2694. if (apiMessageId != null) {
  2695. if (type == Type_CONTACT) {
  2696. return getContactMessageModel(apiMessageId);
  2697. } else if (type == MessageReceiver.Type_GROUP) {
  2698. return getGroupMessageModel(apiMessageId);
  2699. } else if (type == MessageReceiver.Type_DISTRIBUTION_LIST) {
  2700. return getDistributionListMessageModel(apiMessageId);
  2701. }
  2702. }
  2703. return null;
  2704. }
  2705. /*******************************************************************************************
  2706. * Uploader Cache (used to cancel running downloads)
  2707. *******************************************************************************************/
  2708. private final Map<String, BlobUploader> uploaders = new ArrayMap<>();
  2709. private final Map<String, WeakReference<VideoTranscoder>> videoTranscoders = new ArrayMap<>();
  2710. /**
  2711. * create a new AbstractMessageModel uploader
  2712. * a existing uploader will be canceled
  2713. * @param messageModel
  2714. * @param data
  2715. * @return
  2716. */
  2717. private BlobUploader initUploader(AbstractMessageModel messageModel, byte[] data) {
  2718. synchronized (this.uploaders) {
  2719. String key = this.cancelUploader(messageModel);
  2720. BlobUploader up = apiService.createUploader(data);
  2721. this.uploaders.put(key, up);
  2722. logger.debug("create new uploader for message " + key);
  2723. return up;
  2724. }
  2725. }
  2726. private String getLoaderKey(AbstractMessageModel messageModel) {
  2727. return messageModel.getClass().toString() + "-" + messageModel.getUid();
  2728. }
  2729. /**
  2730. * cancel a existing AbstractMessageModel uploader
  2731. * @param messageModel
  2732. * @return
  2733. */
  2734. private String cancelUploader(AbstractMessageModel messageModel) {
  2735. synchronized (this.uploaders) {
  2736. String key = getLoaderKey(messageModel);
  2737. if (this.uploaders.containsKey(key)) {
  2738. logger.debug("cancel upload of message " + key);
  2739. this.uploaders.get(key).cancel();
  2740. this.uploaders.remove(key);
  2741. }
  2742. return key;
  2743. }
  2744. }
  2745. /**
  2746. * cancel an existing video transcoding
  2747. * @param messageModel
  2748. * @return
  2749. */
  2750. private String cancelTranscoding(AbstractMessageModel messageModel) {
  2751. synchronized (this.videoTranscoders) {
  2752. String key = getLoaderKey(messageModel);
  2753. if (this.videoTranscoders.containsKey(key)) {
  2754. logger.debug("cancel transcoding of message " + key);
  2755. WeakReference<VideoTranscoder> videoTranscoderRef = this.videoTranscoders.get(key);
  2756. if (videoTranscoderRef != null) {
  2757. if (videoTranscoderRef.get() != null) {
  2758. videoTranscoderRef.get().cancel();
  2759. }
  2760. }
  2761. this.videoTranscoders.remove(key);
  2762. }
  2763. return key;
  2764. }
  2765. }
  2766. @Override
  2767. public void cancelMessageUpload(AbstractMessageModel messageModel) {
  2768. updateMessageState(messageModel, MessageState.SENDFAILED, null);
  2769. removeSendMachine(messageModel);
  2770. cancelUploader(messageModel);
  2771. }
  2772. @Override
  2773. public void cancelVideoTranscoding(AbstractMessageModel messageModel) {
  2774. updateMessageState(messageModel, MessageState.SENDFAILED, null);
  2775. removeSendMachine(messageModel);
  2776. cancelTranscoding(messageModel);
  2777. }
  2778. /******************************************************************************************
  2779. * Sending Message Machine
  2780. * * Handling sending steps of image/video/audio or file messages
  2781. * * Can be aborted
  2782. ******************************************************************************************/
  2783. public final Map<String, SendMachine> sendMachineInstances = new HashMap<>();
  2784. /**
  2785. * Remove a instantiated sendmachine if exists
  2786. * @param sendMachine
  2787. */
  2788. public void removeSendMachine(SendMachine sendMachine) {
  2789. if(sendMachine != null) {
  2790. sendMachine.abort();
  2791. //remove from instances
  2792. synchronized (this.sendMachineInstances) {
  2793. for(Iterator<Map.Entry<String, SendMachine>> it = this.sendMachineInstances.entrySet().iterator(); it.hasNext(); ) {
  2794. Map.Entry<String, SendMachine> entry = it.next();
  2795. if(entry.getValue() == sendMachine) {
  2796. logger.debug("remove send machine from instance map");
  2797. it.remove();
  2798. }
  2799. }
  2800. }
  2801. }
  2802. }
  2803. public void removeSendMachine(AbstractMessageModel messageModel) {
  2804. if(messageModel == null) {
  2805. //ignore
  2806. return;
  2807. }
  2808. removeSendMachine(getSendMachine(messageModel, false));
  2809. }
  2810. /**
  2811. * get or create a existing send machine
  2812. * @param abstractMessageModel
  2813. * @return
  2814. */
  2815. public SendMachine getSendMachine(AbstractMessageModel abstractMessageModel) {
  2816. return this.getSendMachine(abstractMessageModel, true);
  2817. }
  2818. /**
  2819. * get a send machine or create one (and cache into machine instances)
  2820. * can return NULL
  2821. * @param abstractMessageModel
  2822. * @param createIfNotExists
  2823. * @return
  2824. */
  2825. public SendMachine getSendMachine(AbstractMessageModel abstractMessageModel, boolean createIfNotExists)
  2826. {
  2827. synchronized (this.sendMachineInstances) {
  2828. //be sure to "generate" a unique key
  2829. String key = abstractMessageModel.getClass() + "-" + abstractMessageModel.getUid();
  2830. SendMachine instance = null;
  2831. if(this.sendMachineInstances.containsKey(key)) {
  2832. instance = this.sendMachineInstances.get(key);
  2833. }
  2834. else if(createIfNotExists) {
  2835. instance = new SendMachine();
  2836. this.sendMachineInstances.put(key, instance);
  2837. }
  2838. return instance;
  2839. }
  2840. }
  2841. interface SendMachineProcess {
  2842. void run() throws Exception;
  2843. }
  2844. private class SendMachine {
  2845. private int nextStep = 0;
  2846. private int currentStep = 0;
  2847. private boolean aborted = false;
  2848. public SendMachine reset() {
  2849. this.currentStep = 0;
  2850. return this;
  2851. }
  2852. public SendMachine abort() {
  2853. logger.debug("SendMachine", "aborted");
  2854. this.aborted = true;
  2855. return this;
  2856. }
  2857. public SendMachine next(SendMachineProcess process) throws Exception {
  2858. if(this.aborted) {
  2859. logger.debug("SendMachine", "ignore step, aborted");
  2860. //do nothing
  2861. return this;
  2862. }
  2863. if (this.nextStep == this.currentStep++) {
  2864. try {
  2865. if(process != null) {
  2866. process.run();
  2867. }
  2868. this.nextStep++;
  2869. }
  2870. catch (Exception x) {
  2871. logger.error("Error in send machine", x);
  2872. throw x;
  2873. }
  2874. }
  2875. return this;
  2876. }
  2877. }
  2878. @Override
  2879. public MessageReceiver getMessageReceiver(AbstractMessageModel messageModel) throws ThreemaException {
  2880. if (messageModel instanceof MessageModel) {
  2881. return contactService.createReceiver(contactService.getByIdentity(messageModel.getIdentity()));
  2882. } else if (messageModel instanceof GroupMessageModel) {
  2883. return groupService.createReceiver(groupService.getById(((GroupMessageModel) messageModel).getGroupId()));
  2884. } else if (messageModel instanceof DistributionListMessageModel) {
  2885. DistributionListService ds = ThreemaApplication.getServiceManager().getDistributionListService();
  2886. if (ds != null) {
  2887. return ds.createReceiver(ds.getById(((DistributionListMessageModel) messageModel).getDistributionListId()));
  2888. }
  2889. }
  2890. throw new ThreemaException("No receiver for this message");
  2891. }
  2892. /******************************************************************************************************/
  2893. public interface SendResultListener {
  2894. void onError(String errorMessage);
  2895. void onCompleted();
  2896. }
  2897. /**
  2898. * Send media messages of any kind to an arbitrary number of receivers
  2899. * @param mediaItems List of MediaItems to be sent
  2900. * @param messageReceivers List of MessageReceivers
  2901. * @return AbstractMessageModel of a successfully queued message, null if no message could be queued
  2902. */
  2903. @AnyThread
  2904. @Override
  2905. public void sendMediaAsync(@NonNull List<MediaItem> mediaItems, @NonNull List<MessageReceiver> messageReceivers) {
  2906. sendMediaAsync(mediaItems, messageReceivers, null);
  2907. }
  2908. /**
  2909. * Send media messages of any kind to an arbitrary number of receivers
  2910. * @param mediaItems List of MediaItems to be sent
  2911. * @param messageReceivers List of MessageReceivers
  2912. * @param sendResultListener Listener to notify when messages are queued
  2913. * @return AbstractMessageModel of a successfully queued message, null if no message could be queued
  2914. */
  2915. @AnyThread
  2916. @Override
  2917. public void sendMediaAsync(
  2918. @NonNull final List<MediaItem> mediaItems,
  2919. @NonNull final List<MessageReceiver> messageReceivers,
  2920. @Nullable final SendResultListener sendResultListener
  2921. ) {
  2922. ThreemaApplication.sendMessageExecutorService.submit(() -> {
  2923. sendMedia(mediaItems, messageReceivers, sendResultListener);
  2924. });
  2925. }
  2926. @WorkerThread
  2927. @Override
  2928. public AbstractMessageModel sendMedia(
  2929. @NonNull final List<MediaItem> mediaItems,
  2930. @NonNull final List<MessageReceiver> messageReceivers,
  2931. @Nullable final SendResultListener sendResultListener
  2932. ) {
  2933. AbstractMessageModel successfulMessageModel = null;
  2934. int failedCounter = 0;
  2935. // resolve receivers to account for distribution lists
  2936. final MessageReceiver[] resolvedReceivers = MessageUtil.addDistributionListReceivers(messageReceivers.toArray(new MessageReceiver[0]));
  2937. logger.info("sendMedia: Sending " + mediaItems.size() + " items to " + resolvedReceivers.length + " receivers");
  2938. for (MediaItem mediaItem : mediaItems) {
  2939. logger.info("sendMedia: Now sending item of type " + mediaItem.getType());
  2940. if (TYPE_TEXT == mediaItem.getType()) {
  2941. String text = mediaItem.getCaption();
  2942. if (!TestUtil.empty(text)) {
  2943. for (MessageReceiver messageReceiver : resolvedReceivers) {
  2944. try {
  2945. successfulMessageModel = sendText(text, messageReceiver);
  2946. if (successfulMessageModel != null) {
  2947. logger.info("Text successfully sent");
  2948. } else {
  2949. failedCounter++;
  2950. logger.info("Text send failed");
  2951. }
  2952. } catch (Exception e) {
  2953. failedCounter++;
  2954. logger.error("Could not send text message", e);
  2955. }
  2956. }
  2957. } else {
  2958. failedCounter++;
  2959. logger.info("Text is empty");
  2960. }
  2961. continue;
  2962. }
  2963. final Map<MessageReceiver, AbstractMessageModel> messageModels = new HashMap<>();
  2964. final FileDataModel fileDataModel = createFileDataModel(context, mediaItem);
  2965. if (fileDataModel == null) {
  2966. logger.info("Unable to create FileDataModel");
  2967. failedCounter++;
  2968. continue;
  2969. }
  2970. if (!createMessagesAndSetPending(mediaItem, resolvedReceivers, messageModels, fileDataModel)) {
  2971. logger.info("Unable to create messages");
  2972. failedCounter++;
  2973. continue;
  2974. }
  2975. final byte[] thumbnailData = generateThumbnailData(mediaItem, fileDataModel);
  2976. if (thumbnailData != null) {
  2977. writeThumbnails(messageModels, resolvedReceivers, thumbnailData);
  2978. } else {
  2979. logger.info("Unable to generate thumbnails");
  2980. }
  2981. if (!allChatsArePrivate(resolvedReceivers)) {
  2982. saveToGallery(mediaItem);
  2983. }
  2984. try {
  2985. final byte[] contentData = generateContentData(mediaItem, resolvedReceivers, messageModels, fileDataModel);
  2986. if (contentData != null) {
  2987. if (encryptAndSend(resolvedReceivers, messageModels, fileDataModel, thumbnailData, contentData)) {
  2988. successfulMessageModel = messageModels.get(resolvedReceivers[0]);
  2989. } else {
  2990. throw new ThreemaException("Error encrypting and sending");
  2991. }
  2992. } else {
  2993. logger.info("Error encrypting and sending");
  2994. failedCounter++;
  2995. markAsTerminallyFailed(resolvedReceivers, messageModels);
  2996. }
  2997. } catch (ThreemaException e) {
  2998. if (!(e instanceof TranscodeCanceledException)) {
  2999. logger.error("Exception", e);
  3000. failedCounter++;
  3001. } else {
  3002. logger.info("Video transcoding canceled");
  3003. // canceling is not really a failure
  3004. }
  3005. markAsTerminallyFailed(resolvedReceivers, messageModels);
  3006. }
  3007. }
  3008. if (failedCounter == 0) {
  3009. logger.info("sendMedia: Send successful.");
  3010. sendProfilePicture(resolvedReceivers);
  3011. if (sendResultListener != null) {
  3012. sendResultListener.onCompleted();
  3013. }
  3014. } else {
  3015. final String errorString = context.getString(R.string.an_error_occurred_during_send);
  3016. logger.info(errorString);
  3017. RuntimeUtil.runOnUiThread(() -> Toast.makeText(context, errorString, Toast.LENGTH_LONG).show());
  3018. if (sendResultListener != null) {
  3019. sendResultListener.onError(errorString);
  3020. }
  3021. }
  3022. return successfulMessageModel;
  3023. }
  3024. /**
  3025. * Write thumbnails to local storage
  3026. * @param messageModels
  3027. * @param resolvedReceivers
  3028. * @param thumbnailData
  3029. */
  3030. private void writeThumbnails(Map<MessageReceiver, AbstractMessageModel> messageModels, MessageReceiver[] resolvedReceivers, byte[] thumbnailData) {
  3031. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3032. if (thumbnailData != null) {
  3033. try {
  3034. fileService.writeConversationMediaThumbnail(messageModels.get(messageReceiver), thumbnailData);
  3035. fireOnModifiedMessage(messageModels.get(messageReceiver));
  3036. } catch (Exception ignored) {
  3037. // having no thumbnail is not really fatal
  3038. }
  3039. }
  3040. }
  3041. }
  3042. /**
  3043. * Generate content data for this MediaItem
  3044. * @param mediaItem
  3045. * @param resolvedReceivers
  3046. * @param messageModels
  3047. * @param fileDataModel
  3048. * @return content data as a byte array or null if content data could not be generated
  3049. */
  3050. @WorkerThread
  3051. private @Nullable byte[] generateContentData(@NonNull MediaItem mediaItem,
  3052. @NonNull MessageReceiver[] resolvedReceivers,
  3053. @NonNull Map<MessageReceiver, AbstractMessageModel> messageModels,
  3054. @NonNull FileDataModel fileDataModel) throws ThreemaException {
  3055. switch (mediaItem.getType()) {
  3056. case TYPE_VIDEO:
  3057. // fallthrough
  3058. case TYPE_VIDEO_CAM:
  3059. @VideoTranscoder.TranscoderResult int result = transcodeVideo(mediaItem, resolvedReceivers, messageModels);
  3060. if (result == VideoTranscoder.SUCCESS) {
  3061. return getContentData(mediaItem);
  3062. } else if (result == VideoTranscoder.CANCELED) {
  3063. throw new TranscodeCanceledException();
  3064. }
  3065. break;
  3066. case TYPE_IMAGE:
  3067. // scale and rotate / flip images
  3068. int maxSize = ConfigUtils.getPreferredImageDimensions(mediaItem.getImageScale() == ImageScale_DEFAULT ?
  3069. this.preferenceService.getImageScale() : mediaItem.getImageScale());
  3070. Bitmap bitmap = null;
  3071. try {
  3072. boolean hasNoTransparency = MimeUtil.MIME_TYPE_IMAGE_JPG.equals(mediaItem.getMimeType());
  3073. bitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), maxSize, true, hasNoTransparency);
  3074. if (bitmap != null) {
  3075. bitmap = BitmapUtil.rotateBitmap(bitmap, mediaItem.getExifRotation(), mediaItem.getExifFlip());
  3076. byte[] imageByteArray;
  3077. if (hasNoTransparency) {
  3078. imageByteArray = BitmapUtil.getJpegByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip());
  3079. } else {
  3080. imageByteArray = BitmapUtil.getPngByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip());
  3081. }
  3082. if (imageByteArray != null) {
  3083. fileDataModel.setFileSize(imageByteArray.length);
  3084. return ArrayUtils.concatByteArrays(new byte[NaCl.BOXOVERHEAD], imageByteArray);
  3085. }
  3086. }
  3087. } catch (Exception e) {
  3088. logger.error("Exception", e);
  3089. } finally {
  3090. if (bitmap != null && !bitmap.isRecycled()) {
  3091. bitmap.recycle();
  3092. }
  3093. }
  3094. break;
  3095. case TYPE_IMAGE_CAM:
  3096. // cam images will always be sent in their original size. no scaling needed but possibly rotate and flip
  3097. try (InputStream inputStream = StreamUtil.getFromUri(context, mediaItem.getUri())) {
  3098. if (inputStream != null && inputStream.available() > 0) {
  3099. bitmap = BitmapFactory.decodeStream(new BufferedInputStream(inputStream), null, null);
  3100. if (bitmap != null) {
  3101. bitmap = BitmapUtil.rotateBitmap(BitmapUtil.rotateBitmap(
  3102. bitmap,
  3103. mediaItem.getExifRotation(),
  3104. mediaItem.getExifFlip()), mediaItem.getRotation(), mediaItem.getFlip());
  3105. return ArrayUtils.concatByteArrays(new byte[NaCl.BOXOVERHEAD], BitmapUtil.getJpegByteArray(bitmap, mediaItem.getRotation(), mediaItem.getFlip()));
  3106. }
  3107. }
  3108. } catch (Exception e) {
  3109. logger.error("Exception", e);
  3110. }
  3111. break;
  3112. case TYPE_VOICEMESSAGE:
  3113. // fallthrough
  3114. case TYPE_GIF:
  3115. // fallthrough
  3116. case TYPE_FILE:
  3117. // "regular" file messages
  3118. return getContentData(mediaItem);
  3119. default:
  3120. // media type currently not supported
  3121. break;
  3122. }
  3123. return null;
  3124. }
  3125. /**
  3126. * Generate thumbnail data for this MediaItem
  3127. * @param mediaItem
  3128. * @param fileDataModel
  3129. * @return byte array of the thumbnail bitmap, null if thumbnail could not be generated
  3130. */
  3131. @WorkerThread
  3132. private @Nullable byte[] generateThumbnailData(@NonNull MediaItem mediaItem, @NonNull FileDataModel fileDataModel) {
  3133. Bitmap thumbnailBitmap = null;
  3134. final Map<String, Object> metaData = new HashMap<>();
  3135. int mediaType = mediaItem.getType();
  3136. // we want thumbnails for images and videos even if they are to be sent as files
  3137. if (MimeUtil.isImageFile(fileDataModel.getMimeType())) {
  3138. mediaType = TYPE_IMAGE;
  3139. } else if (MimeUtil.isVideoFile(fileDataModel.getMimeType())) {
  3140. mediaType = TYPE_VIDEO;
  3141. }
  3142. switch (mediaType) {
  3143. case MediaItem.TYPE_VIDEO:
  3144. // fallthrough
  3145. case MediaItem.TYPE_VIDEO_CAM:
  3146. // add duration to metadata
  3147. long trimmedDuration = mediaItem.getDurationMs();
  3148. if (mediaItem.getEndTimeMs() != TIME_UNDEFINED && (mediaItem.getEndTimeMs() != 0L || mediaItem.getStartTimeMs() != 0L)) {
  3149. trimmedDuration = mediaItem.getEndTimeMs() - mediaItem.getStartTimeMs();
  3150. } else {
  3151. if (mediaItem.getDurationMs() == 0) {
  3152. // empty duration means full video
  3153. trimmedDuration = VideoUtil.getVideoDuration(context, mediaItem.getUri());
  3154. mediaItem.setDurationMs(trimmedDuration);
  3155. }
  3156. }
  3157. metaData.put(FileDataModel.METADATA_KEY_DURATION, trimmedDuration / (float) DateUtils.SECOND_IN_MILLIS);
  3158. thumbnailBitmap = IconUtil.getVideoThumbnailFromUri(context, mediaItem);
  3159. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPG);
  3160. break;
  3161. case MediaItem.TYPE_IMAGE:
  3162. // images are always sent as JPGs - so use this for thumbnails - except for stickers which may have a format containing transparency
  3163. BitmapUtil.ExifOrientation exifOrientation = BitmapUtil.getExifOrientation(context, mediaItem.getUri());
  3164. mediaItem.setExifRotation((int) exifOrientation.getRotation());
  3165. mediaItem.setExifFlip(exifOrientation.getFlip());
  3166. boolean hasNoTransparency = MimeUtil.MIME_TYPE_IMAGE_JPG.equals(mediaItem.getMimeType());
  3167. if (hasNoTransparency && mediaItem.getRenderingType() != RENDERING_STICKER) {
  3168. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPG);
  3169. } else {
  3170. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
  3171. }
  3172. thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, true, false, true);
  3173. if (thumbnailBitmap != null) {
  3174. thumbnailBitmap = BitmapUtil.rotateBitmap(BitmapUtil.rotateBitmap(
  3175. thumbnailBitmap,
  3176. mediaItem.getExifRotation(),
  3177. mediaItem.getExifFlip()), mediaItem.getRotation(), mediaItem.getFlip());
  3178. }
  3179. break;
  3180. case MediaItem.TYPE_IMAGE_CAM:
  3181. // camera images are always sent as JPGs
  3182. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPG);
  3183. thumbnailBitmap = BitmapUtil.safeGetBitmapFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, true, false, true);
  3184. if (thumbnailBitmap != null) {
  3185. thumbnailBitmap = BitmapUtil.rotateBitmap(BitmapUtil.rotateBitmap(
  3186. thumbnailBitmap,
  3187. mediaItem.getExifRotation(),
  3188. mediaItem.getExifFlip()), mediaItem.getRotation(), mediaItem.getFlip());
  3189. }
  3190. break;
  3191. case TYPE_GIF:
  3192. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
  3193. thumbnailBitmap = IconUtil.getThumbnailFromUri(context, mediaItem.getUri(), THUMBNAIL_SIZE_PX, fileDataModel.getMimeType(), true);
  3194. break;
  3195. case MediaItem.TYPE_VOICEMESSAGE:
  3196. metaData.put(FileDataModel.METADATA_KEY_DURATION, mediaItem.getDurationMs() / (float) DateUtils.SECOND_IN_MILLIS);
  3197. // voice messages do not have thumbnails
  3198. thumbnailBitmap = null;
  3199. break;
  3200. case MediaItem.TYPE_FILE:
  3201. // just an arbitrary file
  3202. thumbnailBitmap = null;
  3203. break;
  3204. default:
  3205. break;
  3206. }
  3207. fileDataModel.setMetaData(metaData);
  3208. final byte[] thumbnailData;
  3209. if (thumbnailBitmap != null) {
  3210. // convert bitmap to byte array
  3211. if (MimeUtil.MIME_TYPE_IMAGE_JPG.equals(fileDataModel.getThumbnailMimeType())) {
  3212. thumbnailData = BitmapUtil.bitmapToJpegByteArray(thumbnailBitmap);
  3213. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_JPG);
  3214. } else {
  3215. thumbnailData = BitmapUtil.bitmapToPngByteArray(thumbnailBitmap);
  3216. fileDataModel.setThumbnailMimeType(MimeUtil.MIME_TYPE_IMAGE_PNG);
  3217. }
  3218. thumbnailBitmap.recycle();
  3219. } else {
  3220. thumbnailData = null;
  3221. }
  3222. return thumbnailData;
  3223. }
  3224. /**
  3225. * Encrypt content and thumbnail data, upload blobs and queue messages for the specified MediaItem
  3226. * @param resolvedReceivers MessageReceivers to send the MediaItem to
  3227. * @param messageModels MessageModels for above MessageReceivers
  3228. * @param fileDataModel fileDataModel for this message
  3229. * @param thumbnailData Byte Array of thumbnail bitmap to be uploaded as a blob
  3230. * @param contentData Byte Array of Content to be uploaded as a blob
  3231. * @return true if the message was queued successfully, false otherwise. Note that errors that occur during sending are not handled here.
  3232. */
  3233. @WorkerThread
  3234. private boolean encryptAndSend(
  3235. @NonNull MessageReceiver[] resolvedReceivers,
  3236. @NonNull Map<MessageReceiver, AbstractMessageModel> messageModels,
  3237. @NonNull FileDataModel fileDataModel,
  3238. @Nullable byte[] thumbnailData,
  3239. @NonNull byte[] contentData) {
  3240. final MessageReceiver.EncryptResult[] thumbnailEncryptResult = new MessageReceiver.EncryptResult[1];
  3241. final MessageReceiver.EncryptResult[] contentEncryptResult = new MessageReceiver.EncryptResult[1];
  3242. thumbnailEncryptResult[0] = null;
  3243. contentEncryptResult[0] = null;
  3244. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3245. // save content first as it will be modified later on
  3246. AbstractMessageModel messageModel = messageModels.get(messageReceiver);
  3247. if (messageModel == null) {
  3248. // no messagemodel has been created for this receiver - skip
  3249. continue;
  3250. }
  3251. // now set to pending
  3252. messageModel.setState(MessageState.PENDING); // shows a progress bar
  3253. save(messageModel);
  3254. try {
  3255. fileService.writeConversationMedia(messageModel, contentData, NaCl.BOXOVERHEAD, contentData.length - NaCl.BOXOVERHEAD);
  3256. } catch (Exception e) {
  3257. // Failure to write local media is not necessarily fatal, continue
  3258. logger.debug("Exception", e);
  3259. }
  3260. }
  3261. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3262. //enqueue processing and uploading stuff...
  3263. AbstractMessageModel messageModel = messageModels.get(messageReceiver);
  3264. if (messageModel == null) {
  3265. // no messagemodel has been created for this receiver - skip
  3266. logger.info("Mo MessageModel could be created for this receiver - skip");
  3267. continue;
  3268. }
  3269. this.messageSendingService.addToQueue(new MessageSendingService.MessageSendingProcess() {
  3270. public byte[] thumbnailBlobId;
  3271. public byte[] contentBlobId;
  3272. public boolean success = false;
  3273. @Override
  3274. public MessageReceiver getReceiver() {
  3275. return messageReceiver;
  3276. }
  3277. @Override
  3278. public AbstractMessageModel getMessageModel() {
  3279. return messageModel;
  3280. }
  3281. @Override
  3282. public boolean send() throws Exception {
  3283. SendMachine sendMachine = getSendMachine(messageModel);
  3284. sendMachine.reset()
  3285. .next(() -> {
  3286. if (getReceiver().sendMediaData()) {
  3287. // note that encryptFileData will overwrite contents of provided content data!
  3288. if (contentEncryptResult[0] == null) {
  3289. contentEncryptResult[0] = getReceiver().encryptFileData(contentData);
  3290. if (contentEncryptResult[0].getData() == null || contentEncryptResult[0].getSize() == 0) {
  3291. throw new Exception("File data encrypt failed");
  3292. }
  3293. }
  3294. }
  3295. fileDataModel.setFileSize(contentData.length);
  3296. messageModel.setFileData(fileDataModel);
  3297. fireOnModifiedMessage(messageModel);
  3298. })
  3299. .next(() -> {
  3300. //do not upload if sendMediaData Disabled (Distribution Lists)
  3301. if (getReceiver().sendMediaData()) {
  3302. BlobUploader blobUploader = initUploader(getMessageModel(), contentEncryptResult[0].getData());
  3303. blobUploader.setProgressListener(new ProgressListener() {
  3304. @Override
  3305. public void updateProgress(int progress) {
  3306. updateMessageLoadingProgress(messageModel, progress);
  3307. }
  3308. @Override
  3309. public void onFinished(boolean success) {
  3310. setMessageLoadingFinished(messageModel, success);
  3311. }
  3312. });
  3313. contentBlobId = blobUploader.upload();
  3314. logger.debug("blobId = " + Utils.byteArrayToHexString(contentBlobId));
  3315. }
  3316. })
  3317. .next(() -> {
  3318. //do not upload if sendMediaData Disabled (Distribution Lists)
  3319. if (getReceiver().sendMediaData()) {
  3320. if (thumbnailData != null) {
  3321. if (thumbnailEncryptResult[0] == null) {
  3322. thumbnailEncryptResult[0] = getReceiver().encryptFileThumbnailData(thumbnailData, contentEncryptResult[0].getKey());
  3323. }
  3324. if (thumbnailEncryptResult[0].getData() != null) {
  3325. BlobUploader blobUploader = initUploader(getMessageModel(), thumbnailEncryptResult[0].getData());
  3326. blobUploader.setProgressListener(new ProgressListener() {
  3327. @Override
  3328. public void updateProgress(int progress) {
  3329. updateMessageLoadingProgress(messageModel, progress);
  3330. }
  3331. @Override
  3332. public void onFinished(boolean success) {
  3333. setMessageLoadingFinished(messageModel, success);
  3334. }
  3335. });
  3336. thumbnailBlobId = blobUploader.upload();
  3337. logger.debug("blobIdThumbnail = " + Utils.byteArrayToHexString(thumbnailBlobId));
  3338. fireOnModifiedMessage(messageModel);
  3339. } else {
  3340. throw new Exception("Thumbnail encrypt failed");
  3341. }
  3342. }
  3343. }
  3344. })
  3345. .next(() -> {
  3346. if (getReceiver().createBoxedFileMessage(
  3347. thumbnailBlobId,
  3348. contentBlobId,
  3349. contentEncryptResult[0],
  3350. messageModel
  3351. )) {
  3352. updateMessageState(messageModel,
  3353. getReceiver().sendMediaData() && getReceiver().offerRetry() ?
  3354. MessageState.SENDING :
  3355. MessageState.SENT, null);
  3356. messageModel.setFileData(fileDataModel);
  3357. //save updated model
  3358. save(messageModel);
  3359. } else {
  3360. throw new Exception("Failed to create box");
  3361. }
  3362. })
  3363. .next(() -> {
  3364. messageModel.setSaved(true);
  3365. // Verify current saved state
  3366. updateMessageState(messageModel,
  3367. getReceiver().sendMediaData() && getReceiver().offerRetry() ?
  3368. MessageState.SENDING :
  3369. MessageState.SENT, null);
  3370. if (!getReceiver().sendMediaData()) {
  3371. // update status for message that stay local
  3372. fireOnModifiedMessage(messageModel);
  3373. }
  3374. success = true;
  3375. });
  3376. if (this.success) {
  3377. removeSendMachine(sendMachine);
  3378. }
  3379. return this.success;
  3380. }
  3381. });
  3382. }
  3383. return true;
  3384. }
  3385. /**
  3386. * Create MessageModels for all receivers, save local thumbnail and set MessageModels to PENDING for instant UI feedback
  3387. * @param mediaItem
  3388. * @param resolvedReceivers
  3389. * @param messageModels
  3390. * @param fileDataModel
  3391. * @return true if all was hunky dory, false if an error occurred
  3392. */
  3393. @WorkerThread
  3394. private boolean createMessagesAndSetPending(MediaItem mediaItem, MessageReceiver[] resolvedReceivers, Map<MessageReceiver, AbstractMessageModel> messageModels, FileDataModel fileDataModel) {
  3395. String correlationId = getCorrelationId();
  3396. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3397. final AbstractMessageModel messageModel = messageReceiver.createLocalModel(MessageType.FILE, MimeUtil.getContentTypeFromMimeType(fileDataModel.getMimeType()), new Date());
  3398. this.cache(messageModel);
  3399. messageModel.setOutbox(true);
  3400. messageModel.setState(MessageState.PENDING); // shows a progress bar
  3401. messageModel.setFileData(fileDataModel);
  3402. messageModel.setCorrelationId(correlationId);
  3403. messageModel.setCaption(mediaItem.getCaption());
  3404. messageModel.setSaved(true);
  3405. messageReceiver.saveLocalModel(messageModel);
  3406. messageModels.put(messageReceiver, messageModel);
  3407. this.fireOnCreatedMessage(messageModel);
  3408. }
  3409. return true;
  3410. }
  3411. public @Nullable FileDataModel createFileDataModel(Context context, MediaItem mediaItem) {
  3412. ContentResolver contentResolver = context.getContentResolver();
  3413. String mimeType = mediaItem.getMimeType();
  3414. String filename = mediaItem.getFilename();
  3415. if (mediaItem.getUri() == null) {
  3416. return null;
  3417. }
  3418. if (ContentResolver.SCHEME_FILE.equalsIgnoreCase(mediaItem.getUri().getScheme())) {
  3419. if (TestUtil.empty(filename)) {
  3420. File file = new File(mediaItem.getUri().getPath());
  3421. filename = file.getName();
  3422. }
  3423. } else {
  3424. if (TestUtil.empty(filename) || TestUtil.empty(mimeType)) {
  3425. String[] proj = {
  3426. DocumentsContract.Document.COLUMN_DISPLAY_NAME,
  3427. DocumentsContract.Document.COLUMN_MIME_TYPE
  3428. };
  3429. try (Cursor cursor = contentResolver.query(mediaItem.getUri(), proj, null, null, null)) {
  3430. if (cursor != null && cursor.moveToFirst()) {
  3431. if (TestUtil.empty(filename)) {
  3432. filename = cursor.getString(
  3433. cursor.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME));
  3434. }
  3435. if (TestUtil.empty(mimeType) || MimeUtil.MIME_TYPE_DEFAULT.equals(mimeType)) {
  3436. mimeType = cursor.getString(
  3437. cursor.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE));
  3438. }
  3439. }
  3440. } catch (Exception e) {
  3441. logger.error("Unable to query content provider", e);
  3442. }
  3443. }
  3444. }
  3445. if (TestUtil.empty(mimeType) || MimeUtil.MIME_TYPE_DEFAULT.equals(mimeType)) {
  3446. mimeType = FileUtil.getMimeTypeFromUri(context, mediaItem.getUri());
  3447. }
  3448. @FileData.RenderingType int renderingType = mediaItem.getRenderingType();
  3449. // rendering type overrides
  3450. switch (mediaItem.getType()) {
  3451. case TYPE_VOICEMESSAGE:
  3452. filename = FileUtil.getDefaultFilename(mimeType); // the internal temporary file name is of no use to the recipient
  3453. renderingType = FileData.RENDERING_MEDIA;
  3454. break;
  3455. case TYPE_GIF:
  3456. if (renderingType == FileData.RENDERING_DEFAULT) {
  3457. // do not override stickers
  3458. renderingType = FileData.RENDERING_MEDIA;
  3459. }
  3460. break;
  3461. case TYPE_FILE:
  3462. // "regular" file messages
  3463. renderingType = FileData.RENDERING_DEFAULT;
  3464. break;
  3465. default:
  3466. if (mediaItem.getImageScale() == PreferenceService.ImageScale_SEND_AS_FILE) {
  3467. // images with scale type "send as file" get the default rendering type and a file name
  3468. renderingType = FileData.RENDERING_DEFAULT;
  3469. mediaItem.setType(TYPE_FILE);
  3470. } else {
  3471. // unlike with "real" files we override the filename for regular images with a generic one to prevent privacy leaks
  3472. // this mimics the behavior of traditional image messages that did not have a filename at all
  3473. filename = FileUtil.getDefaultFilename(mimeType); // the internal temporary file name is of no use to the recipient
  3474. }
  3475. break;
  3476. }
  3477. if (TestUtil.empty(filename)) {
  3478. filename = FileUtil.getDefaultFilename(mimeType);
  3479. }
  3480. return new FileDataModel(mimeType,
  3481. null,
  3482. 0,
  3483. filename,
  3484. renderingType,
  3485. mediaItem.getCaption(),
  3486. true,
  3487. null);
  3488. }
  3489. /**
  3490. * Transcode and trim this video according to the parameters set in the MediaItem object
  3491. * @param mediaItem
  3492. * @param resolvedReceivers
  3493. * @param messageModels
  3494. * @return Result of transcoding
  3495. */
  3496. @WorkerThread
  3497. private @VideoTranscoder.TranscoderResult int transcodeVideo(MediaItem mediaItem, MessageReceiver[] resolvedReceivers, Map<MessageReceiver, AbstractMessageModel> messageModels) {
  3498. final MessagePlayerService messagePlayerService;
  3499. try {
  3500. messagePlayerService = getServiceManager().getMessagePlayerService();
  3501. } catch (ThreemaException e) {
  3502. logger.error("Exception", e);
  3503. return VideoTranscoder.FAILURE;
  3504. }
  3505. boolean needsTrimming = videoNeedsTrimming(mediaItem);
  3506. int targetBitrate;
  3507. @PreferenceService.VideoSize int desiredVideoSize = preferenceService.getVideoSize();
  3508. if (mediaItem.getVideoSize() != PreferenceService.VideoSize_DEFAULT) {
  3509. desiredVideoSize = mediaItem.getVideoSize();
  3510. }
  3511. try {
  3512. targetBitrate = VideoConfig.getTargetVideoBitrate(context, mediaItem, desiredVideoSize);
  3513. } catch (ThreemaException e) {
  3514. logger.error("Error getting target bitrate", e);
  3515. // skip this MediaItem
  3516. markAsTerminallyFailed(resolvedReceivers, messageModels);
  3517. return VideoTranscoder.FAILURE;
  3518. }
  3519. if (targetBitrate == -1) {
  3520. // will not fit
  3521. logger.info("Video file ist too large");
  3522. // skip this MediaItem
  3523. markAsTerminallyFailed(resolvedReceivers, messageModels);
  3524. return VideoTranscoder.FAILURE;
  3525. }
  3526. logger.info("Target bitrate = {}", targetBitrate);
  3527. if (needsTrimming || targetBitrate > 0) {
  3528. logger.info("Video needs transcoding");
  3529. // set models to TRANSCODING state
  3530. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  3531. AbstractMessageModel messageModel = entry.getValue();
  3532. messageModel.setState(MessageState.TRANSCODING);
  3533. save(messageModel);
  3534. fireOnModifiedMessage(messageModel);
  3535. }
  3536. File outputFile;
  3537. try {
  3538. outputFile = fileService.createTempFile(".trans", ".mp4", !ConfigUtils.useContentUris());
  3539. } catch (IOException e) {
  3540. logger.error("Unable to open temp file");
  3541. // skip this MediaItem
  3542. markAsTerminallyFailed(resolvedReceivers, messageModels);
  3543. return VideoTranscoder.FAILURE;
  3544. }
  3545. final VideoTranscoder.Builder transcoderBuilder = new VideoTranscoder.Builder(mediaItem.getUri(), outputFile);
  3546. if (needsTrimming) {
  3547. transcoderBuilder.trim(mediaItem.getStartTimeMs(), mediaItem.getEndTimeMs());
  3548. }
  3549. if (targetBitrate > 0) {
  3550. int maxSize = VideoConfig.getMaxSizeFromBitrate(targetBitrate);
  3551. transcoderBuilder.maxFrameHeight(maxSize);
  3552. transcoderBuilder.maxFrameWidth(maxSize);
  3553. transcoderBuilder.videoBitRate(targetBitrate);
  3554. transcoderBuilder.iFrameInterval(2);
  3555. transcoderBuilder.frameRate(25); // TODO: variable frame rate
  3556. }
  3557. final VideoTranscoder videoTranscoder = transcoderBuilder.build(context);
  3558. synchronized (this.videoTranscoders) {
  3559. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  3560. AbstractMessageModel messageModel = entry.getValue();
  3561. String key = this.cancelTranscoding(messageModel);
  3562. this.videoTranscoders.put(key, new WeakReference<>(videoTranscoder));
  3563. }
  3564. }
  3565. final @VideoTranscoder.TranscoderResult int transcoderResult = videoTranscoder.startSync(new VideoTranscoder.Listener() {
  3566. @Override
  3567. public void onStart() {
  3568. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  3569. AbstractMessageModel messageModel = entry.getValue();
  3570. messagePlayerService.setTranscodeStart(messageModel);
  3571. }
  3572. }
  3573. @Override
  3574. public void onProgress(int progress) {
  3575. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  3576. AbstractMessageModel messageModel = entry.getValue();
  3577. messagePlayerService.setTranscodeProgress(messageModel, progress);
  3578. }
  3579. }
  3580. @Override
  3581. public void onCanceled() {
  3582. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  3583. AbstractMessageModel messageModel = entry.getValue();
  3584. messagePlayerService.setTranscodeFinished(messageModel, true, null);
  3585. }
  3586. }
  3587. @Override
  3588. public void onSuccess(VideoTranscoder.Stats stats) {
  3589. if (stats != null) {
  3590. logger.debug(stats.toString());
  3591. }
  3592. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  3593. AbstractMessageModel messageModel = entry.getValue();
  3594. messagePlayerService.setTranscodeFinished(messageModel, true, null);
  3595. }
  3596. }
  3597. @Override
  3598. public void onFailure() {
  3599. for (Map.Entry<MessageReceiver, AbstractMessageModel> entry : messageModels.entrySet()) {
  3600. AbstractMessageModel messageModel = entry.getValue();
  3601. messagePlayerService.setTranscodeFinished(messageModel, false, "Failure");
  3602. }
  3603. }
  3604. });
  3605. if (transcoderResult != VideoTranscoder.SUCCESS) {
  3606. // failure
  3607. logger.info("Transcoding failure");
  3608. return transcoderResult;
  3609. }
  3610. // remove original file and set transcoded file as new source file
  3611. deleteTemporaryFile(mediaItem);
  3612. mediaItem.setUri(Uri.fromFile(outputFile));
  3613. } else {
  3614. logger.info("No transcoding necessary");
  3615. }
  3616. return VideoTranscoder.SUCCESS;
  3617. }
  3618. /**
  3619. * Generate a random correlation ID that identifies all media sent in one batch
  3620. * @return correlation Id
  3621. */
  3622. @Override
  3623. public String getCorrelationId() {
  3624. final byte[] random = new byte[16];
  3625. new SecureRandom().nextBytes(random);
  3626. return Utils.byteArrayToHexString(random);
  3627. }
  3628. @WorkerThread
  3629. private void deleteTemporaryFile(MediaItem mediaItem) {
  3630. if (mediaItem.getDeleteAfterUse()) {
  3631. if (mediaItem.getUri() != null && ContentResolver.SCHEME_FILE.equalsIgnoreCase(mediaItem.getUri().getScheme())) {
  3632. if (mediaItem.getUri().getPath() != null) {
  3633. FileUtil.deleteFileOrWarn(mediaItem.getUri().getPath(), null, logger);
  3634. }
  3635. }
  3636. }
  3637. }
  3638. /**
  3639. * Check if all chats in the supplied list of MessageReceivers are set to "hidden"
  3640. * @param messageReceivers
  3641. * @return true if all chats are hidden (i.e. marked as "private"), false if there is at least one chat that is always visible
  3642. */
  3643. private boolean allChatsArePrivate(MessageReceiver[] messageReceivers) {
  3644. for (MessageReceiver messageReceiver : messageReceivers) {
  3645. if (!hiddenChatsListService.has(messageReceiver.getUniqueIdString())) {
  3646. return false;
  3647. }
  3648. }
  3649. return true;
  3650. }
  3651. /**
  3652. * Delete message models for specified receivers
  3653. * @param resolvedReceivers
  3654. * @param messageModels
  3655. */
  3656. private void markAsTerminallyFailed(MessageReceiver[] resolvedReceivers, Map<MessageReceiver, AbstractMessageModel> messageModels) {
  3657. for (MessageReceiver messageReceiver : resolvedReceivers) {
  3658. remove(messageModels.get(messageReceiver));
  3659. }
  3660. }
  3661. /**
  3662. * Get a byte array for the media represented by the MediaItem leaving room for NaCl Box header
  3663. * @param mediaItem MediaItem containing the Uri of the media
  3664. * @return byte array of the media data or null if error occured
  3665. */
  3666. @WorkerThread
  3667. private byte[] getContentData(MediaItem mediaItem) {
  3668. try (InputStream inputStream = StreamUtil.getFromUri(context, mediaItem.getUri())) {
  3669. if (inputStream != null && inputStream.available() > 0) {
  3670. final int fileLength;
  3671. fileLength = inputStream.available();
  3672. if (fileLength > MAX_BLOB_SIZE) {
  3673. logger.info(context.getString(R.string.file_too_large));
  3674. return null;
  3675. }
  3676. if (ConfigUtils.checkAvailableMemory(fileLength + NaCl.BOXOVERHEAD)) {
  3677. try {
  3678. byte[] fileData = new byte[fileLength + NaCl.BOXOVERHEAD];
  3679. IOUtils.readFully(inputStream, fileData, NaCl.BOXOVERHEAD, fileLength);
  3680. return fileData;
  3681. } catch (OutOfMemoryError e) {
  3682. logger.error("Unable to create byte array", e);
  3683. }
  3684. } else {
  3685. logger.info("Not enough memory to create byte array.");
  3686. }
  3687. } else {
  3688. logger.info("Not enough memory to create byte array.");
  3689. }
  3690. } catch (IOException e) {
  3691. logger.error("Unable to open file to send", e);
  3692. }
  3693. return null;
  3694. }
  3695. /**
  3696. * Save outgoing media item recorded from within the app to gallery if enabled
  3697. * @param item
  3698. */
  3699. @WorkerThread
  3700. private void saveToGallery(MediaItem item) {
  3701. if (item.getType() == MediaItem.TYPE_IMAGE_CAM || item.getType() == MediaItem.TYPE_VIDEO_CAM) {
  3702. if (preferenceService.isSaveMedia()) {
  3703. try {
  3704. AbstractMessageModel messageModel = new MessageModel();
  3705. messageModel.setType(item.getType() == TYPE_VIDEO_CAM ? MessageType.VIDEO : MessageType.IMAGE);
  3706. messageModel.setCreatedAt(new Date());
  3707. messageModel.setId(0);
  3708. fileService.copyDecryptedFileIntoGallery(item.getUri(), messageModel);
  3709. } catch (Exception e) {
  3710. logger.error("Exception", e);
  3711. }
  3712. }
  3713. }
  3714. }
  3715. /**
  3716. * Returns true if the user requested trimming of Video referenced by supplied MediaItem
  3717. * @param item MediaItem to check
  3718. * @return true if trimming is required, false otherwise
  3719. */
  3720. private boolean videoNeedsTrimming(MediaItem item) {
  3721. return (item.getStartTimeMs() != 0 && item.getStartTimeMs() != TIME_UNDEFINED) ||
  3722. (item.getEndTimeMs() != TIME_UNDEFINED && item.getEndTimeMs() != item.getDurationMs());
  3723. }
  3724. /******************************************************************************************************/
  3725. }