/*
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.silenttext.activity;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.twuni.twoson.IllegalFormatException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.webkit.MimeTypeMap;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.silentcircle.core.util.StringUtils;
import com.silentcircle.http.client.AbstractHTTPClient;
import com.silentcircle.http.client.CachingHTTPClient;
import com.silentcircle.http.client.HTTPResponse;
import com.silentcircle.http.client.URLBuilder;
import com.silentcircle.http.client.apache.ApacheHTTPClient;
import com.silentcircle.http.client.apache.HttpClient;
import com.silentcircle.http.client.listener.HTTPResponseListener;
import com.silentcircle.scloud.NativePacket;
import com.silentcircle.scloud.PacketInput;
import com.silentcircle.scloud.listener.OnBlockDecryptedListener;
import com.silentcircle.scloud.model.SCloudObject;
import com.silentcircle.silenttext.Extra;
import com.silentcircle.silenttext.R;
import com.silentcircle.silenttext.ServiceConfiguration;
import com.silentcircle.silenttext.application.SilentTextApplication;
import com.silentcircle.silenttext.crypto.Hash;
import com.silentcircle.silenttext.listener.ConfirmDialogNoRepeat;
import com.silentcircle.silenttext.listener.OnConfirmNoRepeatListener;
import com.silentcircle.silenttext.model.Conversation;
import com.silentcircle.silenttext.model.Siren;
import com.silentcircle.silenttext.model.UserPreferences;
import com.silentcircle.silenttext.model.event.Event;
import com.silentcircle.silenttext.model.event.Message;
import com.silentcircle.silenttext.model.io.json.JSONSirenSerializer;
import com.silentcircle.silenttext.repository.ConversationRepository;
import com.silentcircle.silenttext.repository.EventRepository;
import com.silentcircle.silenttext.repository.SCloudObjectRepository;
import com.silentcircle.silenttext.util.AsyncUtils;
import com.silentcircle.silenttext.util.AttachmentUtils;
import com.silentcircle.silenttext.util.IOUtils;
import com.silentcircle.silenttext.util.MIME;
public class SCloudActivity extends SilentActivity {
class AppendToFileOnBlockDecrypted implements OnBlockDecryptedListener {
@Override
public void onBlockDecrypted( byte [] dataBytes, byte [] metaDataBytes ) {
if( metaDataBytes == null || metaDataBytes.length < 1 ) {
pipe( dataBytes );
return;
}
String metaDataString = new String( metaDataBytes );
if( metaDataString.contains( "MimeType" ) && metaDataString.contains( "quicktime" ) ) {
mIsVideoNotSupported = true;
mFormat = "(.MOV/ or quicktime)";
return;
}
getLog().debug( "#onBlockDecrypted metadata:%s", metaDataString );
try {
JSONObject metaData = new JSONObject( metaDataString );
String mediaType = metaData.getString( "MediaType" );
if( metaData.has( "FileName" ) ) {
originalFileName = metaData.getString( "FileName" );
}
if( "com.silentcircle.scloud.segment".equals( mediaType ) ) {
pipe( dataBytes );
return;
}
// TODO: Remove right of && when Voice Mail is correctly MimeType'd
if( metaData.has( "MimeType" ) && !StringUtils.equals( metaData.getString( "MimeType" ), "application/octet-stream" ) ) {
mimeType = metaData.getString( "MimeType" );
} else {
mimeType = MIME.fromUTI( mediaType );
}
int count = metaData.has( "Scloud_Segments" ) ? metaData.getInt( "Scloud_Segments" ) : 1;
if( count <= 1 ) {
pipe( dataBytes );
return;
}
String dataString = new String( dataBytes );
JSONArray index = new JSONArray( dataString );
int progressTotal = 1 + index.length();
for( int i = 0; i < index.length(); i++ ) {
reportProgress( 1 + i, progressTotal );
JSONArray item = index.getJSONArray( i );
String locator = item.getString( 1 );
String key = item.getString( 2 );
SCloudObject object = getSCloudObjectRepository().findById( locator );
if( object == null ) {
object = new SCloudObject( key, locator, null );
}
load( object );
}
reportProgress( progressTotal, progressTotal );
} catch( JSONException exception ) {
pipe( dataBytes );
}
}
private void pipe( byte [] data ) {
if( data == null || data.length < 1 ) {
return;
}
try {
toFileStream.write( data );
} catch( IOException exception ) {
IOUtils.close( toFileStream );
}
}
}
class LoadSCloudObjectTask extends AsyncTask<SCloudObject, Void, Boolean> {
@Override
protected Boolean doInBackground( SCloudObject... objects ) {
try {
for( int i = 0; i < objects.length; i++ ) {
if( isCancelled() ) {
return Boolean.valueOf( false );
}
try {
load( objects[i] );
} catch( Throwable exception ) {
String message = exception.getLocalizedMessage();
if( message != null ) {
toast( R.string.error_format, message.split( "\n" )[0] );
}
Log.e( TAG, exception.getMessage() );
log.error( exception, "#load" );
finish();
return Boolean.valueOf( false );
}
}
} finally {
decryptor.onDestroy();
IOUtils.flush( toFileStream );
IOUtils.close( toFileStream );
}
return Boolean.valueOf( true );
}
@Override
protected void onPostExecute( Boolean success ) {
if( success == null || !success.booleanValue() ) {
return;
}
if( mIsVideoNotSupported ) {
Toast.makeText( SCloudActivity.this, getString( R.string.not_supported_video_format, mFormat ), Toast.LENGTH_LONG ).show();
finish();
return;
}
if( originalFileName == null ) {
if( mimeType != null ) {
originalFileName = String.format( "%s.%s", toFile.getName(), MimeTypeMap.getSingleton().getExtensionFromMimeType( mimeType ) );
}
}
if( originalFileName != null && mimeType == null ) {
String extension = AttachmentUtils.getExtensionFromFileName( originalFileName );
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( extension );
}
if( originalFileName != null ) {
File targetFile = new File( toFile.getParentFile(), originalFileName );
if( toFile.renameTo( targetFile ) ) {
toFile = targetFile;
}
}
Intent intent = new Intent( getActivity(), FileViewerActivity.class );
intent.setDataAndType( Uri.fromFile( toFile ), mimeType );
intent.setFlags( Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP );
startActivity( intent );
finish();
}
}
protected class ProcessIntentOnConfirmListener implements OnConfirmNoRepeatListener, DialogInterface.OnClickListener {
@Override
public void onClick( DialogInterface dialog, int which ) {
if( which == DialogInterface.BUTTON_NEGATIVE ) {
finish();
return;
}
}
@Override
public void onConfirm( Context context, boolean shouldNotShowAgain ) {
UserPreferences preferences = getSilentTextApplication().getUserPreferences();
preferences.ignoreWarningDecryptInternalStore = shouldNotShowAgain;
SilentTextApplication.from( context ).saveUserPreferences( preferences );
processIntent( getIntent() );
}
}
private final static String TAG = "SCloudActivity";
private static String getURL( SCloudObject object ) {
return new URLBuilder( ServiceConfiguration.getInstance().scloud.url ).component( String.valueOf( object.getLocator() ) ).build().toString();
}
boolean mIsVideoNotSupported;
String mFormat = "";
private SCloudObjectRepository scloudObjectRepository;
protected AbstractHTTPClient http;
protected PacketInput decryptor;
protected File toFile;
protected OutputStream toFileStream;
protected String mimeType;
protected final byte [] buffer = new byte [64 * 1024];
protected String originalFileName;
protected boolean cancelled;
@Override
protected String getLogTag() {
return "SCloudActivity";
}
protected SCloudObjectRepository getSCloudObjectRepository() {
return scloudObjectRepository;
}
private void handleIntent() {
handleIntent( getIntent() );
}
private void handleIntent( Intent intent ) {
UserPreferences preferences = getSilentTextApplication().getUserPreferences();
if( !preferences.ignoreWarningDecryptInternalStore ) {
ProcessIntentOnConfirmListener listener = new ProcessIntentOnConfirmListener();
ConfirmDialogNoRepeat alert = new ConfirmDialogNoRepeat( R.string.security_warning, R.string.verify_ok_media_to_be_decrypted, R.string.cancel, R.string._continue, this, listener, listener );
alert.show();
return;
}
processIntent( intent );
}
protected void load( SCloudObject object ) {
try {
loadFromLocalStorage( object );
} catch( Exception exception ) {
loadFromAmazon( object );
}
}
private void loadFromAmazon( final SCloudObject object ) {
object.setURL( getURL( object ) );
object.setUploaded( true );
object.setDownloaded( false );
getSCloudObjectRepository().save( object );
reportProgress( R.string.downloading );
http.get( String.valueOf( object.getURL() ), new HTTPResponseListener() {
@Override
public void onResponse( HTTPResponse response ) {
int status = response.getStatusCode();
InputStream body = null;
try {
body = response.getContent();
ByteArrayOutputStream out = new ByteArrayOutputStream();
for( int size = body.read( buffer ); size > 0; size = body.read( buffer ) ) {
out.write( buffer, 0, size );
}
if( status > 200 ) {
String errorMessage = String.format( "HTTP %d %s\n%s\n%s", Integer.valueOf( status ), response.getStatusReason(), object.getURL(), out.toString() );
throw new RuntimeException( errorMessage );
}
object.setData( out.toByteArray() );
getSCloudObjectRepository().write( object );
object.setDownloaded( true );
getSCloudObjectRepository().save( object );
object.setData( null );
loadFromLocalStorage( object );
} catch( IOException exception ) {
getLog().error( exception, "#loadFromAmazon -> #onResponse" );
} finally {
IOUtils.close( body );
}
}
} );
}
protected void loadFromLocalStorage( SCloudObject object ) {
if( !object.isDownloaded() ) {
throw new RuntimeException( getString( R.string.not_downloaded ) );
}
reportProgress( R.string.decrypting );
scloudObjectRepository.read( object );
// TODO: it would be nice to avoid String when converting from CharSequence to byte array
decryptor.decrypt( object.getData(), String.valueOf( object.getKey() ) );
}
@Override
protected void onCreate( Bundle savedInstanceState ) {
super.onCreate( savedInstanceState );
setContentView( R.layout.loading );
reportProgress( R.string.downloading );
getActionBar().hide();
}
@Override
protected void onNewIntent( Intent intent ) {
super.onNewIntent( intent );
setIntent( intent );
handleIntent( intent );
}
@Override
protected void onResume() {
super.onResume();
try {
assertPermissionToView( this, true, true, true );
} catch( IllegalStateException exception ) {
return;
}
handleIntent();
}
protected void processIntent( Intent intent ) {
String eventID = Extra.ID.from( intent );
String locator = Extra.LOCATOR.from( intent );
String key = Extra.KEY.from( intent );
setSCloudObjects( eventID );
SCloudObject object = getSCloudObjectRepository().findById( locator );
if( object == null ) {
object = new SCloudObject( key, locator, null );
}
File parent = getCacheStagingDir();
parent.mkdirs();
toFile = new File( parent, Hash.sha1( locator ) );
try {
toFileStream = new FileOutputStream( toFile, false );
} catch( IOException exception ) {
IOUtils.close( toFileStream );
return;
}
decryptor = new NativePacket();
decryptor.onCreate();
http = new CachingHTTPClient( new ApacheHTTPClient( new HttpClient() ), getSilentTextApplication().getHTTPResponseCache() );
decryptor.setOnBlockDecryptedListener( new AppendToFileOnBlockDecrypted() );
tasks.add( AsyncUtils.execute( new LoadSCloudObjectTask(), object ) );
}
private void reportProgress( final int labelResourceID ) {
runOnUiThread( new Runnable() {
@Override
public void run() {
TextView progressLabel = (TextView) findViewById( R.id.progress_label );
if( progressLabel != null ) {
progressLabel.setText( labelResourceID );
}
}
} );
}
protected void reportProgress( final int progress, final int total ) {
runOnUiThread( new Runnable() {
@Override
public void run() {
ProgressBar progressBar = (ProgressBar) findViewById( R.id.progress );
if( progressBar != null ) {
progressBar.setMax( total );
progressBar.setProgress( progress );
setVisibleIf( progress > 0 && progress < total, R.id.progress_container );
}
}
} );
}
private void setPreviewImage( Message message ) {
ImageView view = (ImageView) findViewById( R.id.preview );
if( view == null ) {
return;
}
try {
Siren siren = new JSONSirenSerializer().parse( message.getText() );
Bitmap bitmap = AttachmentUtils.getPreviewImage( this, siren );
view.setImageBitmap( bitmap );
view.setVisibility( bitmap == null ? View.GONE : View.VISIBLE );
} catch( IllegalFormatException exception ) {
view.setVisibility( View.GONE );
} catch( IOException exception ) {
view.setVisibility( View.GONE );
}
}
private void setSCloudObjects( Conversation conversation, String eventID ) {
ConversationRepository conversations = getConversations();
EventRepository events = conversations.historyOf( conversation );
Event event = events.findById( eventID );
if( event != null ) {
scloudObjectRepository = events.objectsOf( event );
if( event instanceof Message ) {
setPreviewImage( (Message) event );
}
}
}
private void setSCloudObjects( String eventID ) {
// look up the conversation for this eventID
ConversationRepository conversations = getConversations();
for( Conversation conversation : conversations.list() ) {
setSCloudObjects( conversation, eventID );
}
}
}