/* Copyright (C) 2013-2015, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.silentcircle.scimp; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.lang.ref.SoftReference; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.TimeZone; import java.util.UUID; import org.json.JSONException; import org.json.JSONObject; import android.content.Context; import android.content.Intent; import com.silentcircle.api.model.Key; import com.silentcircle.api.model.Signature; import com.silentcircle.api.model.User; import com.silentcircle.api.web.model.BasicSignature; import com.silentcircle.api.web.model.json.JSONObjectWriter; import com.silentcircle.http.client.CachingHTTPClient; import com.silentcircle.http.client.URLBuilder; import com.silentcircle.http.client.apache.ApacheHTTPClient; import com.silentcircle.http.client.apache.HttpClient; import com.silentcircle.scimp.model.ResourceState; import com.silentcircle.scloud.NativePacket; import com.silentcircle.scloud.listener.OnBlockDecryptedListener; import com.silentcircle.silentstorage.repository.Repository; import com.silentcircle.silentstorage.repository.file.RepositoryLockedException; import com.silentcircle.silentstorage.util.Base64; import com.silentcircle.silenttext.Action; import com.silentcircle.silenttext.Extra; import com.silentcircle.silenttext.Manifest; import com.silentcircle.silenttext.SCimpBridge; import com.silentcircle.silenttext.ServiceConfiguration; import com.silentcircle.silenttext.application.SilentTextApplication; import com.silentcircle.silenttext.crypto.Hash; import com.silentcircle.silenttext.log.Log; import com.silentcircle.silenttext.model.Attachment; import com.silentcircle.silenttext.model.Conversation; import com.silentcircle.silenttext.model.MessageState; import com.silentcircle.silenttext.model.SCIMPError; import com.silentcircle.silenttext.model.Siren; import com.silentcircle.silenttext.model.event.ErrorEvent; import com.silentcircle.silenttext.model.event.Event; import com.silentcircle.silenttext.model.event.IncomingMessage; import com.silentcircle.silenttext.model.event.Message; import com.silentcircle.silenttext.model.event.OutgoingMessage; import com.silentcircle.silenttext.model.event.WarningEvent; import com.silentcircle.silenttext.model.util.SirenUtils; import com.silentcircle.silenttext.receiver.Receiver; import com.silentcircle.silenttext.repository.ConversationRepository; import com.silentcircle.silenttext.repository.EventRepository; import com.silentcircle.silenttext.repository.ResourceStateRepository; import com.silentcircle.silenttext.task.GetUserFromServerTask; import com.silentcircle.silenttext.task.RequestResendTask; import com.silentcircle.silenttext.transport.Envelope; import com.silentcircle.silenttext.transport.TransportQueue; import com.silentcircle.silenttext.util.AsyncUtils; import com.silentcircle.silenttext.util.IOUtils; import com.silentcircle.silenttext.util.KeyUtils; import com.silentcircle.silenttext.util.StringUtils; public class DefaultPacketOutput implements PacketOutput { static class OnMessageDecryptedReceiver implements Receiver<Message> { private final SoftReference<Context> contextReference; private final String data; OnMessageDecryptedReceiver( Context context, String data ) { contextReference = new SoftReference<Context>( context ); this.data = data; } private Context getContext() { return contextReference.get(); } @Override public void onReceive( Message message ) { message.setText( data ); message.setState( MessageState.DECRYPTED ); Siren siren = message.getSiren(); if( siren != null ) { prepareAttachments( siren ); int secondsToLive = siren.getShredAfter(); if( secondsToLive > 0 ) { message.setBurnNotice( secondsToLive ); } } } private void prepareAttachments( Siren siren ) { Context context = getContext(); byte [] locator = siren.getCloudLocatorAsByteArray(); byte [] key = siren.getCloudKeyAsByteArray(); if( context == null || locator == null || key == null ) { return; } String url = new URLBuilder( ServiceConfiguration.getInstance().scloud.url ).component( new String( locator ) ).build().toString(); SilentTextApplication application = SilentTextApplication.from( context ); InputStream response = new CachingHTTPClient( new ApacheHTTPClient( new HttpClient() ), application.getHTTPResponseCache() ).get( url ); NativePacket decryptor = new NativePacket(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); decryptor.setOnBlockDecryptedListener( new OnBlockDecryptedListener() { @Override public void onBlockDecrypted( byte [] data, byte [] metaData ) { try { out.write( metaData ); } catch( IOException exception ) { // Ignore. } } } ); decryptor.decrypt( IOUtils.readFully( response ), new String( key ) ); Attachment attachment = new Attachment(); attachment.setLocator( locator ); attachment.setKey( key ); attachment.setType( siren.getMIMETypeAsByteArray() ); try { JSONObject metaData = new JSONObject( out.toString() ); if( metaData.has( "FileName" ) ) { attachment.setName( StringUtils.toByteArray( metaData.getString( "FileName" ) ) ); } if( attachment.getType() == null && metaData.has( "MediaType" ) ) { attachment.setType( StringUtils.toByteArray( metaData.getString( "MediaType" ) ) ); } if( metaData.has( "FileSize" ) ) { attachment.setSize( metaData.getLong( "FileSize" ) ); } } catch( JSONException exception ) { // Ignore. } application.getAttachments().save( attachment ); } } static class OnMessageEncryptedReceiver implements Receiver<Message> { private final String data; OnMessageEncryptedReceiver( String data ) { this.data = data; } @Override public void onReceive( Message message ) { message.setCiphertext( data ); message.setState( MessageState.ENCRYPTED ); } } private static final int RETRY_ATTEMPTS = 5; // from SCimp.h private static int kSCimpState_Init = 0; private static int kSCimpState_Ready = 1; private static int kSCimpState_Error = 2; private static String createID( String key ) { return String.format( "%s:%d", key, Long.valueOf( System.currentTimeMillis() ) ); } private static void flushErrors( EventRepository events ) { if( events == null || !events.exists() ) { return; } List<Event> history = events.list(); for( int i = 0; i < history.size(); i++ ) { Event event = history.get( i ); if( event instanceof ErrorEvent || event instanceof WarningEvent ) { events.remove( event ); } } } private static Collection<OutgoingMessage> getPendingOutgoingMessages( EventRepository events, String exceptPacketID ) { Collection<OutgoingMessage> pending = new ArrayList<OutgoingMessage>(); for( Event event : events.list() ) { if( event.getId().equals( exceptPacketID ) ) { continue; } if( event instanceof OutgoingMessage ) { OutgoingMessage message = (OutgoingMessage) event; if( MessageState.COMPOSED.equals( message.getState() ) && message.hasText() ) { pending.add( message ); } } } return pending; } static boolean isKeyingInProgress( int state ) { if( state == kSCimpState_Init || state == kSCimpState_Ready || state == kSCimpState_Error ) { return false; } return true; } private static void markPacketAsDelivered( EventRepository events, String deliveredPacketID, long deliveryTime ) { Event event = events.findById( deliveredPacketID ); if( event instanceof OutgoingMessage ) { OutgoingMessage message = (OutgoingMessage) event; message.setState( MessageState.DELIVERED ); message.setDeliveryTime( deliveryTime ); events.save( message ); } } public static String requestResend( Context context, String packetID ) { if( packetID == null ) { return null; } JSONObject resendJSON = new JSONObject(); SilentTextApplication application = SilentTextApplication.from( context ); Repository<NamedKeyPair> namedKeyPairRepository = application.getNamedKeyPairs(); if( namedKeyPairRepository != null ) { NamedKeyPair userKeyPair = namedKeyPairRepository.list().fetchAll().get( 0 ); if( userKeyPair != null ) { Key userPublicKey = KeyUtils.extractPublicKey( userKeyPair ); if( userPublicKey != null ) { List<Signature> userKeySignatures = userPublicKey.getSignatures(); if( userKeySignatures != null && userKeySignatures.size() > 0 ) { Signature userKeySignature = userKeySignatures.get( 0 ); ByteArrayOutputStream out = new ByteArrayOutputStream(); try { JSONObjectWriter.writeSignature( (BasicSignature) userKeySignature, new DataOutputStream( out ) ); if( out.size() != 0 ) { resendJSON.put( "siren_sig_v2", out.toString() ); } } catch( IOException e ) { // Fall through. } catch( JSONException e ) { // Fall through. } } } } } try { resendJSON.put( "request_resend", packetID ); return resendJSON.toString(); } catch( JSONException impossible ) { return null; } } private static String unwrapSCimpPacket( String encoded ) { return StringUtils.fromByteArray( Base64.decodeBase64( encoded.replaceAll( "^\\?SCIMP:(.+)\\.$", "$1" ) ) ); } private final SoftReference<Context> contextReference; protected static final Log LOG = new Log( "SCimp" ); private static final SimpleDateFormat ISO8601 = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss'Z'" ); static { ISO8601.setTimeZone( TimeZone.getTimeZone( "UTC" ) ); } public DefaultPacketOutput( Context context ) { contextReference = new SoftReference<Context>( context ); } private void burnPacket( Conversation conversation, String remoteUserID, String burnedPacketID ) { ConversationRepository conversations = getConversations(); EventRepository events = conversations.historyOf( conversation ); Event event = events.findById( burnedPacketID ); if( event == null ) { return; } if( !( event instanceof IncomingMessage ) ) { return; } IncomingMessage message = (IncomingMessage) event; if( !remoteUserID.equals( message.getSender() ) ) { return; } events.remove( event ); getApplication().removeAttachments( event ); if( event.getId().equals( conversation.getPreviewEventID() ) ) { if( event instanceof IncomingMessage && MessageState.DECRYPTED.equals( ( (IncomingMessage) event ).getState() ) ) { conversation.offsetUnreadMessageCount( -1 ); } List<Event> history = events.list(); int count = history.size(); if( count > 0 ) { conversation.setPreviewEventID( history.get( count - 1 ).getId() ); } else { conversation.setPreviewEventID( (byte []) null ); } conversations.save( conversation ); } } private boolean consumePacket( String remoteUserID, String data ) { Siren siren = SirenUtils.parse( data ); if( siren.isPing() ) { // Ignore this for now. return true; } ConversationRepository conversations = getConversations(); Conversation conversation = conversations.findByPartner( remoteUserID ); if( siren.isBurnRequest() ) { burnPacket( conversation, remoteUserID, siren.getRequestBurn() ); return true; } EventRepository events = conversations.historyOf( conversation ); if( siren.isResendRequest() ) { resendPacket( events, siren.getRequestResend() ); return true; } if( siren.isAcknowledgment() ) { markPacketAsDelivered( events, siren.getAcknowledgment(), siren.getDeliveryTimestamp() ); return true; } return false; } private Conversation findConversation( String remoteUserID ) { ConversationRepository conversations = getConversations(); if( conversations == null || !conversations.exists() ) { return null; } return conversations.findByPartner( remoteUserID ); } private EventRepository findConversationHistory( String remoteUserID ) { ConversationRepository conversations = getConversations(); if( conversations != null ) { Conversation conversation = conversations.findByPartner( remoteUserID ); if( conversation != null ) { return conversations.historyOf( conversation ); } } return null; } private ResourceState findResourceState( Conversation conversation ) { ConversationRepository conversations = getConversations(); if( conversations == null || !conversations.exists() ) { return null; } ResourceStateRepository states = conversations.contextOf( conversation ); String resource = conversation.getPartner().getDevice(); if( resource == null ) { resource = ""; } ResourceState state = states.findById( resource ); if( state == null ) { state = new ResourceState(); state.setResource( resource ); } return state; } private void flush( String remoteUserID, Collection<OutgoingMessage> messages ) { if( messages != null ) { for( OutgoingMessage message : messages ) { getNative().encrypt( remoteUserID, message.getId(), message.getText(), true, true ); } } } SilentTextApplication getApplication() { return (SilentTextApplication) getContext().getApplicationContext(); } private Context getContext() { return contextReference.get(); } private ConversationRepository getConversations() { return getApplication().getConversations(); } private SCimpBridge getNative() { return getApplication().getSCimpBridge(); } PacketInput getNativeInput() { return getNative().getInput(); } private TransportQueue getOutgoingMessageQueue() { return getApplication().getOutgoingMessageQueue(); } private void handleError( final byte [] storageKey, final String packetID, final String localUserID, final String remoteUserID, int errorCode, final int state ) { final ConversationRepository conversations = getConversations(); final Conversation conversation = conversations.findByPartner( remoteUserID ); if( conversation != null ) { conversation.offsetFailures( 1 ); SCIMPError error = SCIMPError.forValue( errorCode ); if( conversation.getFailures() > RETRY_ATTEMPTS ) { LOG.error( "#onError error:%s failures:%d max_retries:%d (retry limit exceeded)", error, Integer.valueOf( conversation.getFailures() ), Integer.valueOf( RETRY_ATTEMPTS ) ); } conversations.save( conversation ); final ResourceStateRepository states = conversations.contextOf( conversation ); final ResourceState resourceState = states.findById( conversation.getPartner().getDevice() ); switch( error ) { case CORRUPT_DATA: // Refresh user info. AsyncUtils.execute( new GetUserFromServerTask( getApplication(), false ) { @Override protected void onPostExecute( User user ) { // We got a message we couldn't decrypt. Let's hold onto the message ID // and // queue up a resend request for after we have established keys. AsyncUtils.execute( new RequestResendTask( getApplication(), packetID, localUserID, remoteUserID ) { @Override protected void onPostExecute( Void result ) { // Our storage key could not decrypt the // context. Let's just use // the new storage // key create a new context and begin the keying // process. states.remove( resourceState ); if( conversation.getFailures() <= RETRY_ATTEMPTS && !isKeyingInProgress( state ) ) { // attempt to reconnect if not already // reconnecting getNativeInput().connect( storageKey, null, localUserID, remoteUserID, null ); } } } ); } }, remoteUserID ); break; case KEY_NOT_FOUND: // Refresh user info. AsyncUtils.execute( new GetUserFromServerTask( getApplication(), false ) { @Override protected void onPostExecute( User user ) { AsyncUtils.execute( new RequestResendTask( getApplication(), packetID, localUserID, remoteUserID ) { @Override protected void onPostExecute( Void result ) { if( conversation.getFailures() <= RETRY_ATTEMPTS && !isKeyingInProgress( state ) ) { // attempt to reconnect if not already reconnecting getNativeInput().connect( storageKey, null, localUserID, remoteUserID, resourceState == null ? null : resourceState.getState() ); } } } ); } }, remoteUserID ); break; case PROTOCOL_ERROR: case BAD_INTEGRITY: // Refresh user info. AsyncUtils.execute( new GetUserFromServerTask( getApplication(), false ) { @Override protected void onPostExecute( User user ) { // We got a message we couldn't decrypt. Let's hold onto the message ID // and // queue up a resend request for after we have established keys. AsyncUtils.execute( new RequestResendTask( getApplication(), packetID, localUserID, remoteUserID ) { @Override protected void onPostExecute( Void result ) { // Our storage key could not decrypt the // context. Let's just use // the new storage // key create a new context and begin the keying // process. states.remove( resourceState ); if( conversation.getFailures() <= RETRY_ATTEMPTS && !isKeyingInProgress( state ) ) { // attempt to reconnect if not already // reconnecting getNativeInput().connect( storageKey, null, localUserID, remoteUserID, null ); } } } ); } }, remoteUserID ); break; case NOT_CONNECTED: AsyncUtils.execute( new GetUserFromServerTask( getApplication(), false ) { @Override protected void onPostExecute( User user ) { if( conversation.getFailures() <= RETRY_ATTEMPTS && !isKeyingInProgress( state ) ) { // attempt to reconnect if not already reconnecting, with packet // encryption // following through automatically getNativeInput().connect( storageKey, null, localUserID, remoteUserID, resourceState == null ? null : resourceState.getState() ); } else if( conversation.getFailures() > RETRY_ATTEMPTS ) { states.remove( resourceState ); getNativeInput().connect( storageKey, null, localUserID, remoteUserID, null ); } } }, remoteUserID ); break; default: // Something else happened that we didn't expect. Since this is an error, let's // drop the secure context. states.remove( resourceState ); break; } } if( ServiceConfiguration.getInstance().debug ) { saveError( errorCode, remoteUserID ); } invalidate( remoteUserID ); } /** * @param storageKey * @param packetID * @param localUserID * @param remoteUserID * @param warningCode * @param state */ private void handleWarning( byte [] storageKey, String packetID, String localUserID, String remoteUserID, int warningCode, int state ) { ConversationRepository conversations = getConversations(); Conversation conversation = conversations.findByPartner( remoteUserID ); if( conversation != null ) { conversation.offsetFailures( 1 ); conversations.save( conversation ); ResourceStateRepository states = conversations.contextOf( conversation ); ResourceState resourceState = states.findById( conversation.getPartner().getDevice() ); switch( SCIMPError.forValue( warningCode ) ) { case SECRETS_MISMATCH: // We had already established a secure conversation, and we're still secure, but // our SAS phrase has changed. // TODO: Exactly how does this happen, and is it normal? states.setVerified( resourceState, false ); break; default: // Something else happened that we didn't expect. Just drop the packet without // affecting the existing context. break; } } invalidate( remoteUserID ); } private void invalidate( String remoteUserID ) { Intent intent = Action.UPDATE_CONVERSATION.intent(); Extra.PARTNER.to( intent, remoteUserID ); getContext().sendBroadcast( intent, Manifest.permission.READ ); } @Override public void onConnect( byte [] storageKey, String packetID, String localUserID, String remoteUserID, String context, String secret ) { LOG.debug( "CONNECT id:%s to:%s context:%s %s", packetID, remoteUserID, Hash.sha1( context ), secret ); processPacket( remoteUserID, context, null, secret, null ); invalidate( remoteUserID ); } @Override public void onError( byte [] storageKey, String packetID, String localUserID, String remoteUserID, int errorCode, int state ) { LOG.error( "ERROR id:%s user:%s %s", packetID, remoteUserID, SCIMPError.forValue( errorCode ) ); handleError( storageKey, packetID, localUserID, remoteUserID, errorCode, state ); } @Override public void onReceivePacket( byte [] storageKey, String packetID, String localUserID, String remoteUserID, byte [] dataBytes, String context, String secret, boolean notifiable, boolean badgeworthy ) { final String data = StringUtils.fromByteArray( dataBytes ); LOG.debug( "RECV id:%s from:%s notifiable:%b badgeworthy:%b context:%s\n%s", packetID, remoteUserID, Boolean.valueOf( notifiable ), Boolean.valueOf( badgeworthy ), Hash.sha1( context ), data ); if( consumePacket( remoteUserID, data ) ) { saveState( remoteUserID, context, secret ); savePacket( remoteUserID, packetID, data, MessageState.DECRYPTED ); } else { processPacket( remoteUserID, context, packetID, secret, new OnMessageDecryptedReceiver( getContext(), data ) ); } invalidate( remoteUserID ); } @Override public void onSendPacket( byte [] storageKey, String packetID, String localUserID, String remoteUserID, byte [] dataBytes, String context, String secret, boolean notifiable, boolean badgeworthy ) { final String data = StringUtils.fromByteArray( dataBytes ); LOG.debug( "SEND id:%s to:%s notifiable:%b badgeworthy:%b context:%s\n%s\n%s", packetID, remoteUserID, Boolean.valueOf( notifiable ), Boolean.valueOf( badgeworthy ), Hash.sha1( context ), data, unwrapSCimpPacket( data ) ); processPacket( remoteUserID, context, packetID, secret, new OnMessageEncryptedReceiver( data ) ); Envelope envelope = new Envelope(); envelope.time = System.currentTimeMillis(); envelope.id = packetID == null ? UUID.randomUUID().toString() : packetID; envelope.from = localUserID; envelope.to = remoteUserID; envelope.content = data; envelope.notifiable = notifiable; envelope.badgeworthy = badgeworthy; envelope.state = Envelope.State.PENDING; try { getOutgoingMessageQueue().add( envelope ); } catch( RepositoryLockedException exception ) { LOG.warn( exception, "#onSendPacket id:%s to:%s", envelope.id, envelope.to ); } invalidate( remoteUserID ); } @Override public void onStateTransition( byte [] storageKey, String localUserID, String remoteUserID, int toState ) { // TODO Auto-generated method stub } @Override public void onWarning( byte [] storageKey, String packetID, String localUserID, String remoteUserID, int warningCode, int state ) { LOG.warn( "WARN id:%s user:%s %s", packetID, remoteUserID, SCIMPError.forValue( warningCode ) ); if( !SCIMPError.SECRETS_MISMATCH.equals( SCIMPError.forValue( warningCode ) ) && ServiceConfiguration.getInstance().debug ) { saveWarning( warningCode, remoteUserID ); } handleWarning( storageKey, packetID, localUserID, remoteUserID, warningCode, state ); } private void processPacket( String remoteUserID, String context, String packetID, String secret, Receiver<Message> receiver ) { Conversation conversation = findConversation( remoteUserID ); if( conversation == null ) { return; } ResourceState state = findResourceState( conversation ); String previousVerifyCode = state.getVerifyCode(); saveState( conversation, context, secret ); if( packetID != null ) { conversation.setPreviewEventID( packetID ); } ConversationRepository conversations = getConversations(); EventRepository events = conversations.historyOf( conversation ); Collection<OutgoingMessage> pending = null; if( state.isSecure() ) { conversation.setFailures( 0 ); if( secret != null && !secret.equals( previousVerifyCode ) ) { // Record this: we have just secured the conversation. flushErrors( events ); pending = getPendingOutgoingMessages( events, packetID ); } } if( packetID == null ) { conversation.setLastModified( System.currentTimeMillis() ); conversations.save( conversation ); flush( remoteUserID, pending ); return; } if( receiver != null ) { Event event = events.findById( packetID ); if( event == null ) { event = new IncomingMessage( remoteUserID, packetID, null ); event.setConversationID( remoteUserID ); } if( event instanceof Message ) { Message message = (Message) event; message.setTime( System.currentTimeMillis() ); receiver.onReceive( message ); if( MessageState.DECRYPTED.equals( message.getState() ) ) { conversation.offsetUnreadMessageCount( 1 ); } if( !SilentTextApplication.isOutgoingResendRequest( message ) ) { // EA: don't save resend requests because they stack up as blank views in the // conversation // FIXME: pull this condition out if we fix that, 'cuz they should get saved in // case we receive repeats later events.save( message ); } } } conversation.setLastModified( System.currentTimeMillis() ); conversations.save( conversation ); transition( remoteUserID, packetID ); flush( remoteUserID, pending ); } private void resendPacket( EventRepository events, String requestedPacketID ) { Event event = events.findById( requestedPacketID ); if( event instanceof OutgoingMessage ) { // STA-1023 - Bypass handling of this from the Activity to this class // OutgoingMessage message = (OutgoingMessage) event; // message.setState( MessageState.RESEND_REQUESTED ); // events.save( message ); OutgoingMessage message = (OutgoingMessage) event; String self = getApplication().getUsername(); Conversation conversation = getApplication().getConversations().findById( message.getConversationID() ); if( self != null && self.equals( conversation.getPartner().getUsername() ) ) { // Talking to self return; } // Purge current message event, set a new ID for the message, and push it out as a // composed message events.remove( message ); message.setId( UUID.randomUUID().toString() ); message.setState( MessageState.COMPOSED ); events.save( message ); transition( conversation.getPartner().getUsername(), event.getId() ); } } private void save( String remoteUserID, Event event ) { ConversationRepository conversations = getConversations(); if( conversations != null ) { Conversation conversation = conversations.findByPartner( remoteUserID ); if( conversation != null ) { EventRepository events = conversations.historyOf( conversation ); if( events != null ) { events.save( event ); } } } } private void saveError( int code, String remoteUserID ) { saveErrorOrWarning( new ErrorEvent( code ), remoteUserID, "ERROR" ); } private void saveErrorOrWarning( Event event, String remoteUserID, String identifierPrefix ) { event.setId( createID( identifierPrefix ) ); event.setConversationID( remoteUserID ); save( remoteUserID, event ); } private void savePacket( String remoteUserID, String packetID, String data, MessageState decrypted ) { EventRepository events = findConversationHistory( remoteUserID ); if( events != null ) { Event event = events.findById( packetID ); if( event instanceof Message ) { Message message = (Message) event; message.setText( data ); message.setState( decrypted ); events.save( message ); } } } private void saveState( Conversation conversation, String context, String secret ) { if( conversation == null ) { return; } ConversationRepository conversations = getConversations(); if( conversations != null ) { if( conversations.exists() ) { ResourceStateRepository states = conversations.contextOf( conversation ); if( states != null ) { ResourceState state = findResourceState( conversation ); if( context != null ) { state.setState( context ); } else { LOG.warn( "#saveState conversation_id:%s context:(null)", conversation.getId() ); } state.setVerifyCode( secret ); states.save( state ); } } } } private void saveState( String remoteUserID, String context, String secret ) { Conversation conversation = findConversation( remoteUserID ); if( conversation != null ) { saveState( conversation, context, secret ); } } private void saveWarning( int code, String remoteUserID ) { saveErrorOrWarning( new WarningEvent( code ), remoteUserID, "WARN" ); } private void transition( String remoteUserID, String packetID ) { Intent intent = Action.TRANSITION.intent(); Extra.PARTNER.to( intent, remoteUserID ); Extra.ID.to( intent, packetID ); getContext().sendBroadcast( intent, Manifest.permission.READ ); } }