package com.alexbbb.uploadservice;
import android.annotation.SuppressLint;
import android.app.IntentService;
import android.app.NotificationManager;
import android.content.Intent;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.Builder;
import android.util.Log;
import java.io.BufferedReader;
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;
/**
* 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";
protected static final String PARAM_CUSTOM_USER_AGENT = "customUserAgent";
protected static final String PARAM_MAX_RETRIES = "maxRetries";
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";
private NotificationManager notificationManager;
private Builder notification;
private PowerManager.WakeLock wakeLock;
private UploadNotificationConfig notificationConfig;
private int lastPublishedProgress;
// indicates if the upload request should be continued
private static volatile boolean shouldContinue = true;
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.putExtra(PARAM_CUSTOM_USER_AGENT, task.getCustomUserAgent());
intent.putExtra(PARAM_MAX_RETRIES, task.getMaxRetries());
intent.putParcelableArrayListExtra(PARAM_FILES, task.getFilesToUpload());
intent.putParcelableArrayListExtra(PARAM_REQUEST_HEADERS, task.getHeaders());
intent.putParcelableArrayListExtra(PARAM_REQUEST_PARAMETERS, task.getParameters());
task.getContext().startService(intent);
}
}
/**
* Stops the currently active upload task.
*/
public static void stopCurrentUpload() {
shouldContinue = false;
}
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 String customUserAgent = intent.getStringExtra(PARAM_CUSTOM_USER_AGENT);
final int maxRetries = intent.getIntExtra(PARAM_MAX_RETRIES, 0);
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;
shouldContinue = true;
wakeLock.acquire();
int attempts = 0;
createNotification();
while (attempts <= maxRetries && shouldContinue) {
attempts++;
try {
handleFileUpload(uploadId, url, method, files, headers, parameters, customUserAgent);
} catch (Exception exc) {
if (attempts > maxRetries || !shouldContinue)
broadcastError(uploadId, exc);
else
Log.w(getClass().getName(), "Error in uploadId " + uploadId + " on attempt " + attempts,
exc);
}
}
}
}
}
@SuppressLint("NewApi")
private
void
handleFileUpload(final String uploadId, final String url, final String method,
final ArrayList<FileToUpload> filesToUpload, final ArrayList<NameValue> requestHeaders,
final ArrayList<NameValue> requestParameters, final String customUserAgent)
throws IOException {
final String boundary = getBoundary();
final byte[] boundaryBytes = getBoundaryBytes(boundary);
HttpURLConnection conn = null;
OutputStream requestStream = null;
InputStream responseStream = null;
try {
// get the content length of the entire HTTP/Multipart request body
long parameterBytes = getRequestParametersBytes(requestParameters, boundaryBytes.length);
final long totalFileBytes = getFileBytes(filesToUpload, boundaryBytes.length);
final byte[] trailer = getTrailerBytes(boundary);
final long bodyLength = parameterBytes + totalFileBytes + trailer.length;
if (android.os.Build.VERSION.SDK_INT < 19 && bodyLength > Integer.MAX_VALUE)
throw new IOException("You need Android API version 19 or newer to "
+ "upload more than 2GB in a single request using "
+ "fixed size content length. Try switching to "
+ "chunked mode instead, but make sure your server side supports it!");
conn = getMultipartHttpURLConnection(url, method, boundary, filesToUpload.size());
if (customUserAgent != null && !customUserAgent.equals("")) {
requestHeaders.add(new NameValue("User-Agent", customUserAgent));
}
setRequestHeaders(conn, requestHeaders);
if (android.os.Build.VERSION.SDK_INT >= 19) {
conn.setFixedLengthStreamingMode(bodyLength);
} else {
conn.setFixedLengthStreamingMode((int) bodyLength);
}
requestStream = conn.getOutputStream();
setRequestParameters(requestStream, requestParameters, boundaryBytes);
uploadFiles(uploadId, requestStream, filesToUpload, boundaryBytes);
requestStream.write(trailer, 0, trailer.length);
final int serverResponseCode = conn.getResponseCode();
if (serverResponseCode / 100 == 2) {
responseStream = conn.getInputStream();
} else { // getErrorStream if the response code is not 2xx
responseStream = conn.getErrorStream();
}
final String serverResponseMessage = getResponseBodyAsString(responseStream);
broadcastCompleted(uploadId, serverResponseCode, serverResponseMessage);
} finally {
closeOutputStream(requestStream);
closeInputStream(responseStream);
closeConnection(conn);
}
}
private String getResponseBodyAsString(final InputStream inputStream) {
StringBuilder outString = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
outString.append(line);
}
} catch (Exception exc) {
try {
if (reader != null)
reader.close();
} catch (Exception readerExc) {
}
}
return outString.toString();
}
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, int totalFiles) throws IOException {
final HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod(method);
if (totalFiles <= 1) {
conn.setRequestProperty("Connection", "close");
} else {
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
long
getRequestParametersBytes(final ArrayList<NameValue> requestParameters, final long boundaryBytesLength)
throws UnsupportedEncodingException {
long parametersBytes = 0;
if (!requestParameters.isEmpty()) {
for (final NameValue parameter : requestParameters) {
// the bytes needed for every parameter are the sum of the boundary bytes
// and the bytes occupied by the parameter. Check setRequestParameters method
parametersBytes += boundaryBytesLength + parameter.getBytes().length;
}
}
return parametersBytes;
}
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) {
if (!shouldContinue)
continue;
requestStream.write(boundaryBytes, 0, boundaryBytes.length);
byte[] headerBytes = file.getMultipartHeader();
requestStream.write(headerBytes, 0, headerBytes.length);
final InputStream stream = file.getStream();
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
try {
while ((bytesRead = stream.read(buffer, 0, buffer.length)) > 0 && shouldContinue) {
requestStream.write(buffer, 0, bytesRead);
uploadedBytes += bytesRead;
broadcastProgress(uploadId, uploadedBytes, totalBytes);
}
} finally {
closeInputStream(stream);
}
}
}
private long getTotalBytes(final ArrayList<FileToUpload> filesToUpload) {
long total = 0;
for (FileToUpload file : filesToUpload) {
total += file.length();
}
return total;
}
private
long
getFileBytes(final ArrayList<FileToUpload> filesToUpload, long boundaryBytesLength)
throws UnsupportedEncodingException {
long total = 0;
for (FileToUpload file : filesToUpload) {
total += file.getTotalMultipartBytes(boundaryBytesLength);
}
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 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);
sendBroadcast(intent);
try{
wakeLock.release();
}
catch (Exception e) {
}
}
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);
try{
wakeLock.release();
}
catch (Exception e) {
}
}
private void createNotification() {
notification.setContentTitle(notificationConfig.getTitle()).setContentText(notificationConfig.getMessage())
.setContentIntent(notificationConfig.getPendingIntent(this))
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(100, 0, true).setOngoing(true);
startForeground(UPLOAD_NOTIFICATION_ID, notification.build());
}
private void updateNotificationProgress(final int progress) {
notification.setContentTitle(notificationConfig.getTitle()).setContentText(notificationConfig.getMessage())
.setContentIntent(notificationConfig.getPendingIntent(this))
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(100, progress, false)
.setOngoing(true);
startForeground(UPLOAD_NOTIFICATION_ID, notification.build());
}
private void updateNotificationCompleted() {
stopForeground(notificationConfig.isAutoClearOnSuccess());
if (!notificationConfig.isAutoClearOnSuccess()) {
notification.setContentTitle(notificationConfig.getTitle())
.setContentText(notificationConfig.getCompleted())
.setContentIntent(notificationConfig.getPendingIntent(this))
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(0, 0, false).setOngoing(false);
notificationManager.notify(UPLOAD_NOTIFICATION_ID_DONE, notification.build());
}
}
private void updateNotificationError() {
stopForeground(false);
notification.setContentTitle(notificationConfig.getTitle()).setContentText(notificationConfig.getError())
.setContentIntent(notificationConfig.getPendingIntent(this))
.setSmallIcon(notificationConfig.getIconResourceID()).setProgress(0, 0, false).setOngoing(false);
notificationManager.notify(UPLOAD_NOTIFICATION_ID_DONE, notification.build());
}
}