package net.wigle.wigleandroid.background;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import net.wigle.wigleandroid.MainActivity;
import android.os.Handler;
/**
* Based on http://getablogger.blogspot.com/2008/01/android-how-to-post-file-to-php-server.html
* Read more: http://getablogger.blogspot.com/2008/01/android-how-to-post-file-to-php-server.html#ixzz0iqTJF7SV
*/
final class HttpFileUploader {
public static final String ENCODING = "UTF-8";
public static final String LINE_END = "\r\n";
public static final String TWO_HYPHENS = "--";
/** don't allow construction */
private HttpFileUploader(){
}
/**
* upload utility method.
*
* @param urlString the url to POST the file to
* @param filename the filename to use for the post
* @param fileParamName the HTML form field name for the file
* @param fileInputStream an open file stream to the file to post
* @param params form data fields (key and value)
* @param handler if non-null gets empty messages with updates on progress
* @param filesize guess at filesize for UI callbacks
*/
public static String upload( final String urlString, final String filename, final String fileParamName,
final FileInputStream fileInputStream, final Map<String,String> params,
final PreConnectConfigurator preConnectConfigurator,
final Handler handler, final long filesize)
throws IOException {
String retval = null;
HttpURLConnection conn = null;
try {
final boolean setBoundary = true;
conn = AbstractApiRequest.connect(urlString, setBoundary, preConnectConfigurator,
ApiDownloader.REQUEST_POST);
if (conn == null) {
throw new IOException("No connection for: " + urlString);
}
OutputStream connOutputStream = conn.getOutputStream();
// reflect out the chunking info
for ( Method meth : connOutputStream.getClass().getMethods() ) {
// MainActivity.info("meth: " + meth.getName() );
try {
if ( "isCached".equals(meth.getName()) || "isChunked".equals(meth.getName())) {
Boolean val = (Boolean) meth.invoke( connOutputStream, (Object[]) null );
MainActivity.info( meth.getName() + " " + val );
}
else if ( "size".equals( meth.getName())) {
Integer val = (Integer) meth.invoke( connOutputStream, (Object[]) null );
MainActivity.info( meth.getName() + " " + val );
}
}
catch ( Exception ex ) {
// this block is just for logging, so don't splode if it has a problem
MainActivity.error("ex: " + ex, ex );
}
}
WritableByteChannel wbc = Channels.newChannel( connOutputStream );
StringBuilder header = new StringBuilder( 400 ); // find a better guess. it was 281 for me in the field 2010/05/16 -hck
for ( Map.Entry<String, String> entry : params.entrySet() ) {
header.append( TWO_HYPHENS ).append( AbstractApiRequest.BOUNDARY ).append( LINE_END );
header.append("Content-Disposition: form-data; name=\"")
.append(entry.getKey()).append("\"").append(LINE_END);
header.append( LINE_END );
header.append( entry.getValue() );
header.append( LINE_END );
}
header.append( TWO_HYPHENS + AbstractApiRequest.BOUNDARY + LINE_END );
header.append("Content-Disposition: form-data; name=\"").append(fileParamName)
.append("\";filename=\"").append(filename).append("\"").append(LINE_END);
header.append( "Content-Type: application/octet_stream" + LINE_END );
header.append( LINE_END );
MainActivity.info( "About to write headers, length: " + header.length() );
CharsetEncoder enc = Charset.forName( ENCODING ).newEncoder();
CharBuffer cbuff = CharBuffer.allocate( 1024 );
ByteBuffer bbuff = ByteBuffer.allocate( 1024 );
writeString( wbc, header.toString(), enc, cbuff, bbuff );
MainActivity.info( "Headers are written, length: " + header.length() );
int percentTimesTenDone = ( header.length() * 1000) / (int)filesize;
if ( handler != null && percentTimesTenDone >= 0 ) {
handler.sendEmptyMessage( BackgroundGuiHandler.WRITING_PERCENT_START + percentTimesTenDone );
}
FileChannel fc = fileInputStream.getChannel();
long byteswritten = 0;
final int chunk = 16 * 1024;
while ( byteswritten < filesize ) {
final long bytes = fc.transferTo( byteswritten, chunk, wbc );
if ( bytes <= 0 ) {
MainActivity.info( "giving up transferring file. bytes: " + bytes );
break;
}
byteswritten += bytes;
MainActivity.info( "transferred " + byteswritten + " of " + filesize );
percentTimesTenDone = ((int)byteswritten * 1000) / (int)filesize;
if ( handler != null && percentTimesTenDone >= 0 ) {
handler.sendEmptyMessage( BackgroundGuiHandler.WRITING_PERCENT_START + percentTimesTenDone );
}
}
MainActivity.info( "done. transferred " + byteswritten + " of " + filesize );
// send multipart form data necesssary after file data...
header.setLength( 0 ); // clear()
header.append(LINE_END);
header.append(TWO_HYPHENS + AbstractApiRequest.BOUNDARY + TWO_HYPHENS + LINE_END);
writeString( wbc, header.toString(), enc, cbuff, bbuff );
// close streams
MainActivity.info( "File is written" );
wbc.close();
fc.close();
fileInputStream.close();
int responseCode = conn.getResponseCode();
MainActivity.info( "connection response code: " + responseCode );
// read the response
final InputStream is = getInputStream( conn );
int ch;
final StringBuilder b = new StringBuilder();
final byte[] buffer = new byte[1024];
while( ( ch = is.read( buffer ) ) != -1 ) {
b.append( new String( buffer, 0, ch, ENCODING ) );
}
retval = b.toString();
// MainActivity.info( "Response: " + retval );
}
finally {
if ( conn != null ) {
MainActivity.info( "conn disconnect" );
conn.disconnect();
}
}
return retval;
}
/**
* get the InputStream, gunzip'ing if needed
*/
public static InputStream getInputStream( HttpURLConnection conn ) throws IOException {
InputStream input = conn.getInputStream();
String encode = conn.getContentEncoding();
MainActivity.info( "Encoding: " + encode );
if ( "gzip".equalsIgnoreCase( encode ) ) {
input = new GZIPInputStream( input );
}
return input;
}
/**
* write a string out to a byte channel.
*
* @param wbc the byte channel to write to
* @param str the string to write
* @param enc the cbc encoder to use, will be reset
* @param cbuff the scratch charbuffer, will be cleared
* @param bbuff the scratch bytebuffer, will be cleared
*/
private static void writeString( WritableByteChannel wbc, String str, CharsetEncoder enc, CharBuffer cbuff, ByteBuffer bbuff ) throws IOException {
// clear existing state
cbuff.clear();
bbuff.clear();
enc.reset();
cbuff.put( str );
cbuff.flip();
CoderResult result = enc.encode( cbuff, bbuff, true );
if ( CoderResult.UNDERFLOW != result ) {
throw new IOException( "encode fail. result: " + result + " cbuff: " + cbuff + " bbuff: " + bbuff );
}
result = enc.flush( bbuff );
if ( CoderResult.UNDERFLOW != result ) {
throw new IOException( "flush fail. result: " + result + " bbuff: " + bbuff );
}
bbuff.flip();
int remaining = bbuff.remaining();
while ( remaining > 0 ) {
remaining -= wbc.write( bbuff );
}
}
}