package com.alexbbb.uploadservice;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
/**
* Service to upload files as a multi-part form data in background using HTTP POST with notification center progress
* display.
*
* @author alexbbb (Alex Gotev)
* @author eliasnaur
*/
public class UploadService extends IntentService {
private static final String SERVICE_NAME = UploadService.class.getName();
private static final String TAG = "AndroidUploadService";
private static final int UPLOAD_NOTIFICATION_ID = 1234; // Something unique
private static final int UPLOAD_NOTIFICATION_ID_DONE = 1235; // Something unique
private static final int BUFFER_SIZE = 4096;
private static final String NEW_LINE = "\r\n";
private static final String TWO_HYPHENS = "--";
public static String NAMESPACE = "com.alexbbb";
private static final String ACTION_UPLOAD_SUFFIX = ".uploadservice.action.upload";
protected static final String PARAM_NOTIFICATION_CONFIG = "notificationConfig";
protected static final String PARAM_ID = "id";
protected static final String PARAM_URL = "url";
protected static final String PARAM_METHOD = "method";
protected static final String PARAM_FILES = "files";
protected static final String PARAM_REQUEST_HEADERS = "requestHeaders";
protected static final String PARAM_REQUEST_PARAMETERS = "requestParameters";
private static final String BROADCAST_ACTION_SUFFIX = ".uploadservice.broadcast.status";
public static final String UPLOAD_ID = "id";
public static final String STATUS = "status";
public static final int STATUS_IN_PROGRESS = 1;
public static final int STATUS_COMPLETED = 2;
public static final int STATUS_ERROR = 3;
public static final String PROGRESS = "progress";
public static final String ERROR_EXCEPTION = "errorException";
public static final String SERVER_RESPONSE_CODE = "serverResponseCode";
public static final String SERVER_RESPONSE_MESSAGE = "serverResponseMessage";
public static final String SERVER_RESPONSE_CONTENT = "serverResponseContent";
private NotificationManager notificationManager;
private Builder notification;
private PowerManager.WakeLock wakeLock;
private UploadNotificationConfig notificationConfig;
private int lastPublishedProgress;
public static String getActionUpload() {
return NAMESPACE + ACTION_UPLOAD_SUFFIX;
}
public static String getActionBroadcast() {
return NAMESPACE + BROADCAST_ACTION_SUFFIX;
}
/**
* Utility method that creates the intent that starts the background file upload service.
*
* @param task object containing the upload request
* @throws IllegalArgumentException if one or more arguments passed are invalid
* @throws MalformedURLException if the server URL is not valid
*/
public static void startUpload(final UploadRequest task) throws IllegalArgumentException, MalformedURLException {
if (task == null) {
throw new IllegalArgumentException("Can't pass an empty task!");
} else {
task.validate();
final Intent intent = new Intent(task.getContext(), UploadService.class);
intent.setAction(getActionUpload());
intent.putExtra(PARAM_NOTIFICATION_CONFIG, task.getNotificationConfig());
intent.putExtra(PARAM_ID, task.getUploadId());
intent.putExtra(PARAM_URL, task.getServerUrl());
intent.putExtra(PARAM_METHOD, task.getMethod());
intent.putParcelableArrayListExtra(PARAM_FILES, task.getFilesToUpload());
intent.putParcelableArrayListExtra(PARAM_REQUEST_HEADERS, task.getHeaders());
intent.putParcelableArrayListExtra(PARAM_REQUEST_PARAMETERS, task.getParameters());
task.getContext().startService(intent);
}
}
public UploadService() {
super(SERVICE_NAME);
}
@Override
public void onCreate() {
super.onCreate();
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notification = new NotificationCompat.Builder(this);
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (getActionUpload().equals(action)) {
notificationConfig = intent.getParcelableExtra(PARAM_NOTIFICATION_CONFIG);
final String uploadId = intent.getStringExtra(PARAM_ID);
final String url = intent.getStringExtra(PARAM_URL);
final String method = intent.getStringExtra(PARAM_METHOD);
final ArrayList<FileToUpload> files = intent.getParcelableArrayListExtra(PARAM_FILES);
final ArrayList<NameValue> headers = intent.getParcelableArrayListExtra(PARAM_REQUEST_HEADERS);
final ArrayList<NameValue> parameters = intent.getParcelableArrayListExtra(PARAM_REQUEST_PARAMETERS);
lastPublishedProgress = 0;
wakeLock.acquire();
try {
createNotification();
handleFileUpload(uploadId, url, method, files, headers, parameters);
} catch (Exception exception) {
broadcastError(uploadId, exception);
} finally {
wakeLock.release();
}
}
}
}
private void handleFileUpload(final String uploadId, final String url, final String method,
final ArrayList<FileToUpload> filesToUpload,
final ArrayList<NameValue> requestHeaders,
final ArrayList<NameValue> requestParameters) throws IOException {
final String boundary = getBoundary();
final byte[] boundaryBytes = getBoundaryBytes(boundary);
HttpURLConnection conn = null;
OutputStream requestStream = null;
InputStream readStream = null;
try {
conn = getMultipartHttpURLConnection(url, method, boundary);
setRequestHeaders(conn, requestHeaders);
requestStream = conn.getOutputStream();
setRequestParameters(requestStream, requestParameters, boundaryBytes);
uploadFiles(uploadId, requestStream, filesToUpload, boundaryBytes);
final byte[] trailer = getTrailerBytes(boundary);
requestStream.write(trailer, 0, trailer.length);
final int serverResponseCode = conn.getResponseCode();
final String serverResponseMessage = conn.getResponseMessage();
readStream = conn.getInputStream();
String serverResponseContent = readResponseContent(readStream);
broadcastCompleted(uploadId, serverResponseCode, serverResponseMessage, serverResponseContent);
} finally {
closeInputStream(readStream);
closeOutputStream(requestStream);
closeConnection(conn);
}
}
private String readResponseContent(InputStream readStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(readStream));
StringBuffer buffer = new StringBuffer();
String data = null;
while ((data = reader.readLine()) != null) {
buffer.append(data);
}
String serverResponseContent = buffer.toString();
return serverResponseContent;
}
private String getBoundary() {
final StringBuilder builder = new StringBuilder();
builder.append("---------------------------").append(System.currentTimeMillis());
return builder.toString();
}
private byte[] getBoundaryBytes(final String boundary) throws UnsupportedEncodingException {
final StringBuilder builder = new StringBuilder();
builder.append(NEW_LINE).append(TWO_HYPHENS).append(boundary).append(NEW_LINE);
return builder.toString().getBytes("US-ASCII");
}
private byte[] getTrailerBytes(final String boundary) throws UnsupportedEncodingException {
final StringBuilder builder = new StringBuilder();
builder.append(NEW_LINE).append(TWO_HYPHENS).append(boundary).append(TWO_HYPHENS).append(NEW_LINE);
return builder.toString().getBytes("US-ASCII");
}
private HttpURLConnection getMultipartHttpURLConnection(final String url, final String method, final String boundary) throws IOException {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setChunkedStreamingMode(0);
conn.setRequestMethod(method);
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("ENCTYPE", "multipart/form-data");
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
return conn;
}
private void setRequestHeaders(final HttpURLConnection conn, final ArrayList<NameValue> requestHeaders) {
if (!requestHeaders.isEmpty()) {
for (final NameValue param : requestHeaders) {
conn.setRequestProperty(param.getName(), param.getValue());
}
}
}
private void setRequestParameters(final OutputStream requestStream, final ArrayList<NameValue> requestParameters,
final byte[] boundaryBytes) throws IOException, UnsupportedEncodingException {
if (!requestParameters.isEmpty()) {
for (final NameValue parameter : requestParameters) {
requestStream.write(boundaryBytes, 0, boundaryBytes.length);
byte[] formItemBytes = parameter.getBytes();
requestStream.write(formItemBytes, 0, formItemBytes.length);
}
}
}
private void uploadFiles(final String uploadId, final OutputStream requestStream,
final ArrayList<FileToUpload> filesToUpload, final byte[] boundaryBytes) throws UnsupportedEncodingException, IOException, FileNotFoundException {
final long totalBytes = getTotalBytes(filesToUpload);
long uploadedBytes = 0;
for (FileToUpload file : filesToUpload) {
requestStream.write(boundaryBytes, 0, boundaryBytes.length);
byte[] headerBytes = file.getMultipartHeader();
requestStream.write(headerBytes, 0, headerBytes.length);
final InputStream stream = compress(file);
byte[] buffer = new byte[BUFFER_SIZE];
long bytesRead;
try {
while ((bytesRead = stream.read(buffer, 0, buffer.length)) > 0) {
requestStream.write(buffer, 0, buffer.length);
uploadedBytes += bytesRead;
broadcastProgress(uploadId, uploadedBytes, totalBytes);
}
} finally {
closeInputStream(stream);
}
}
}
private InputStream compress(FileToUpload file) throws FileNotFoundException {
InputStream stream = null;
File f = file.getFile();
if (f != null) {
String filename = f.getAbsolutePath();
try {
Bitmap bm = ImageResizer.decodeSampledBitmapFromFile(filename, 240, 320);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 100, baos);
stream = new ByteArrayInputStream(baos.toByteArray());
} catch (OutOfMemoryError e) {
e.printStackTrace();
System.gc();
stream = file.getStream();
} catch (Exception e) {
e.printStackTrace();
stream = file.getStream();
}
} else {
stream = file.getStream();
}
return stream;
}
private long getTotalBytes(final ArrayList<FileToUpload> filesToUpload) {
long total = 0;
for (FileToUpload file : filesToUpload) {
total += file.length();
}
return total;
}
private void closeInputStream(final InputStream stream) {
if (stream != null) {
try {
stream.close();
} catch (Exception exc) {
}
}
}
private void closeOutputStream(final OutputStream stream) {
if (stream != null) {
try {
stream.flush();
stream.close();
} catch (Exception exc) {
}
}
}
private void closeConnection(final HttpURLConnection connection) {
if (connection != null) {
try {
connection.disconnect();
} catch (Exception exc) {
}
}
}
private void broadcastProgress(final String uploadId, final long uploadedBytes, final long totalBytes) {
final int progress = (int) (uploadedBytes * 100 / totalBytes);
if (progress <= lastPublishedProgress)
return;
lastPublishedProgress = progress;
updateNotificationProgress(progress);
final Intent intent = new Intent(getActionBroadcast());
intent.putExtra(UPLOAD_ID, uploadId);
intent.putExtra(STATUS, STATUS_IN_PROGRESS);
intent.putExtra(PROGRESS, progress);
sendBroadcast(intent);
}
private void broadcastCompleted(final String uploadId, final int responseCode, final String responseMessage, final String responseContent) {
final String filteredMessage;
if (responseMessage == null) {
filteredMessage = "";
} else {
filteredMessage = responseMessage;
}
if (responseCode >= 200 && responseCode <= 299)
updateNotificationCompleted();
else
updateNotificationError();
final Intent intent = new Intent(getActionBroadcast());
intent.putExtra(UPLOAD_ID, uploadId);
intent.putExtra(STATUS, STATUS_COMPLETED);
intent.putExtra(SERVER_RESPONSE_CODE, responseCode);
intent.putExtra(SERVER_RESPONSE_MESSAGE, filteredMessage);
intent.putExtra(SERVER_RESPONSE_CONTENT, responseContent);
sendBroadcast(intent);
}
private void broadcastError(final String uploadId, final Exception exception) {
updateNotificationError();
final Intent intent = new Intent(getActionBroadcast());
intent.setAction(getActionBroadcast());
intent.putExtra(UPLOAD_ID, uploadId);
intent.putExtra(STATUS, STATUS_ERROR);
intent.putExtra(ERROR_EXCEPTION, exception);
sendBroadcast(intent);
}
private void createNotification() {
if (notificationConfig != null) {
notification.setContentTitle(notificationConfig.getTitle()).setContentText(notificationConfig.getMessage())
.setContentIntent(PendingIntent.getBroadcast(this, 0, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT))
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(100, 0, true).setOngoing(true);
startForeground(UPLOAD_NOTIFICATION_ID, notification.build());
}
}
private void updateNotificationProgress(final int progress) {
if (notificationConfig != null) {
notification.setContentTitle(notificationConfig.getTitle()).setContentText(notificationConfig.getMessage())
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(100, progress, false)
.setOngoing(true);
startForeground(UPLOAD_NOTIFICATION_ID, notification.build());
}
}
private void updateNotificationCompleted() {
if (notificationConfig != null) {
stopForeground(notificationConfig.isAutoClearOnSuccess());
if (!notificationConfig.isAutoClearOnSuccess()) {
notification.setContentTitle(notificationConfig.getTitle())
.setContentText(notificationConfig.getCompleted())
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(0, 0, false).setOngoing(false);
notificationManager.notify(UPLOAD_NOTIFICATION_ID_DONE, notification.build());
}
}
}
private void updateNotificationError() {
if (notificationConfig != null) {
stopForeground(false);
notification.setContentTitle(notificationConfig.getTitle()).setContentText(notificationConfig.getError())
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(0, 0, false).setOngoing(false);
notificationManager.notify(UPLOAD_NOTIFICATION_ID_DONE, notification.build());
}
}
}